Skip to content

Commit 3d5f20b

Browse files
committed
multi: introduce BlindedPaymentPathSet
This commit introduces a new type, `BlindedPaymentPathSet`. For now, it holds only a single `BlindedPayment` but eventually it will hold and manage a set of blinded payments provided for a specific payment. To make the PR easier to follow though, we start off just letting it hold a single one and do some basic replacements.
1 parent 67c5fa9 commit 3d5f20b

File tree

3 files changed

+207
-59
lines changed

3 files changed

+207
-59
lines changed

lnrpc/routerrpc/router_backend.go

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
280280
var (
281281
targetPubKey *route.Vertex
282282
routeHintEdges map[route.Vertex][]routing.AdditionalEdge
283-
blindedPmt *routing.BlindedPayment
283+
blindedPathSet *routing.BlindedPaymentPathSet
284284

285285
// finalCLTVDelta varies depending on whether we're sending to
286286
// a blinded route or an unblinded node. For blinded paths,
@@ -297,13 +297,14 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
297297
// Validate that the fields provided in the request are sane depending
298298
// on whether it is using a blinded path or not.
299299
if len(in.BlindedPaymentPaths) > 0 {
300-
blindedPmt, err = parseBlindedPayment(in)
300+
blindedPathSet, err = parseBlindedPaymentPaths(in)
301301
if err != nil {
302302
return nil, err
303303
}
304304

305-
if blindedPmt.Features != nil {
306-
destinationFeatures = blindedPmt.Features.Clone()
305+
pathFeatures := blindedPathSet.Features()
306+
if pathFeatures != nil {
307+
destinationFeatures = pathFeatures.Clone()
307308
}
308309
} else {
309310
// If we do not have a blinded path, a target pubkey must be
@@ -390,7 +391,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
390391
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
391392
CltvLimit: cltvLimit,
392393
DestFeatures: destinationFeatures,
393-
BlindedPayment: blindedPmt,
394+
BlindedPayment: blindedPathSet.GetPath(),
394395
}
395396

396397
// Pass along an outgoing channel restriction if specified.
@@ -419,39 +420,24 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
419420

420421
return routing.NewRouteRequest(
421422
sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
422-
customRecords, routeHintEdges, blindedPmt, finalCLTVDelta,
423+
customRecords, routeHintEdges, blindedPathSet.GetPath(),
424+
finalCLTVDelta,
423425
)
424426
}
425427

426-
func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
427-
*routing.BlindedPayment, error) {
428+
func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) (
429+
*routing.BlindedPaymentPathSet, error) {
428430

429431
if len(in.PubKey) != 0 {
430432
return nil, fmt.Errorf("target pubkey: %x should not be set "+
431433
"when blinded path is provided", in.PubKey)
432434
}
433435

434-
if len(in.BlindedPaymentPaths) != 1 {
435-
return nil, errors.New("query routes only supports a single " +
436-
"blinded path")
437-
}
438-
439-
blindedPath := in.BlindedPaymentPaths[0]
440-
441436
if len(in.RouteHints) > 0 {
442437
return nil, errors.New("route hints and blinded path can't " +
443438
"both be set")
444439
}
445440

446-
blindedPmt, err := unmarshalBlindedPayment(blindedPath)
447-
if err != nil {
448-
return nil, fmt.Errorf("parse blinded payment: %w", err)
449-
}
450-
451-
if err := blindedPmt.Validate(); err != nil {
452-
return nil, fmt.Errorf("invalid blinded path: %w", err)
453-
}
454-
455441
if in.FinalCltvDelta != 0 {
456442
return nil, errors.New("final cltv delta should be " +
457443
"zero for blinded paths")
@@ -466,7 +452,21 @@ func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
466452
"be populated in blinded path")
467453
}
468454

469-
return blindedPmt, nil
455+
paths := make([]*routing.BlindedPayment, len(in.BlindedPaymentPaths))
456+
for i, paymentPath := range in.BlindedPaymentPaths {
457+
blindedPmt, err := unmarshalBlindedPayment(paymentPath)
458+
if err != nil {
459+
return nil, fmt.Errorf("parse blinded payment: %w", err)
460+
}
461+
462+
if err := blindedPmt.Validate(); err != nil {
463+
return nil, fmt.Errorf("invalid blinded path: %w", err)
464+
}
465+
466+
paths[i] = blindedPmt
467+
}
468+
469+
return routing.NewBlindedPaymentPathSet(paths)
470470
}
471471

472472
func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
@@ -1001,28 +1001,24 @@ func (r *RouterBackend) extractIntentFromSendRequest(
10011001
payIntent.Metadata = payReq.Metadata
10021002

10031003
if len(payReq.BlindedPaymentPaths) > 0 {
1004-
// NOTE: Currently we only choose a single payment path.
1005-
// This will be updated in a future PR to handle
1006-
// multiple blinded payment paths.
1007-
path := payReq.BlindedPaymentPaths[0]
1008-
if len(path.Hops) == 0 {
1009-
return nil, fmt.Errorf("a blinded payment " +
1010-
"must have at least 1 hop")
1004+
pathSet, err := BuildBlindedPathSet(
1005+
payReq.BlindedPaymentPaths,
1006+
)
1007+
if err != nil {
1008+
return nil, err
10111009
}
1010+
payIntent.BlindedPayment = pathSet.GetPath()
10121011

1013-
finalHop := path.Hops[len(path.Hops)-1]
1014-
1015-
payIntent.BlindedPayment = MarshalBlindedPayment(path)
1016-
1017-
// Replace the target node with the blinded public key
1018-
// of the blinded path's final node.
1012+
// Replace the target node with the target public key
1013+
// of the blinded path set.
10191014
copy(
10201015
payIntent.Target[:],
1021-
finalHop.BlindedNodePub.SerializeCompressed(),
1016+
pathSet.TargetPubKey().SerializeCompressed(),
10221017
)
10231018

1024-
if !path.Features.IsEmpty() {
1025-
payIntent.DestFeatures = path.Features.Clone()
1019+
pathFeatures := pathSet.Features()
1020+
if !pathFeatures.IsEmpty() {
1021+
payIntent.DestFeatures = pathFeatures.Clone()
10261022
}
10271023
}
10281024
} else {
@@ -1163,9 +1159,29 @@ func (r *RouterBackend) extractIntentFromSendRequest(
11631159
return payIntent, nil
11641160
}
11651161

1166-
// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
1162+
// BuildBlindedPathSet marshals a set of zpay32.BlindedPaymentPath and uses
1163+
// the result to build a new routing.BlindedPaymentPathSet.
1164+
func BuildBlindedPathSet(paths []*zpay32.BlindedPaymentPath) (
1165+
*routing.BlindedPaymentPathSet, error) {
1166+
1167+
marshalledPaths := make([]*routing.BlindedPayment, len(paths))
1168+
for i, path := range paths {
1169+
paymentPath := marshalBlindedPayment(path)
1170+
1171+
err := paymentPath.Validate()
1172+
if err != nil {
1173+
return nil, err
1174+
}
1175+
1176+
marshalledPaths[i] = paymentPath
1177+
}
1178+
1179+
return routing.NewBlindedPaymentPathSet(marshalledPaths)
1180+
}
1181+
1182+
// marshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
11671183
// routing.BlindedPayment.
1168-
func MarshalBlindedPayment(
1184+
func marshalBlindedPayment(
11691185
path *zpay32.BlindedPaymentPath) *routing.BlindedPayment {
11701186

11711187
return &routing.BlindedPayment{

routing/blinding.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/btcsuite/btcd/btcec/v2"
78
sphinx "github.com/lightningnetwork/lightning-onion"
89
"github.com/lightningnetwork/lnd/channeldb/models"
910
"github.com/lightningnetwork/lnd/lnwire"
@@ -25,6 +26,141 @@ var (
2526
ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
2627
)
2728

29+
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
30+
// blinded paths provided by the recipient of a payment.
31+
//
32+
// NOTE: for now this only holds a single BlindedPayment. By the end of the PR
33+
// series, it will handle multiple paths.
34+
type BlindedPaymentPathSet struct {
35+
// paths is the set of blinded payment paths for a single payment.
36+
// NOTE: For now this will always only have a single entry. By the end
37+
// of this PR, it can hold multiple.
38+
paths []*BlindedPayment
39+
40+
// targetPubKey is the ephemeral node pub key that we will inject into
41+
// each path as the last hop. This is only for the sake of path finding.
42+
// Once the path has been found, the original destination pub key is
43+
// used again. In the edge case where there is only a single hop in the
44+
// path (the introduction node is the destination node), then this will
45+
// just be the introduction node's real public key.
46+
targetPubKey *btcec.PublicKey
47+
48+
// features is the set of relay features available for the payment.
49+
// This is extracted from the set of blinded payment paths. At the
50+
// moment we require that all paths for the same payment have the
51+
// same feature set.
52+
features *lnwire.FeatureVector
53+
}
54+
55+
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
56+
// BlindedPayments.
57+
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
58+
error) {
59+
60+
if len(paths) == 0 {
61+
return nil, ErrNoBlindedPath
62+
}
63+
64+
// For now, we assert that all the paths have the same set of features.
65+
features := paths[0].Features
66+
noFeatures := features == nil || features.IsEmpty()
67+
for i := 1; i < len(paths); i++ {
68+
noFeats := paths[i].Features == nil ||
69+
paths[i].Features.IsEmpty()
70+
71+
if noFeatures && !noFeats {
72+
return nil, fmt.Errorf("all blinded paths must have " +
73+
"the same set of features")
74+
}
75+
76+
if noFeatures {
77+
continue
78+
}
79+
80+
if !features.RawFeatureVector.Equals(
81+
paths[i].Features.RawFeatureVector,
82+
) {
83+
84+
return nil, fmt.Errorf("all blinded paths must have " +
85+
"the same set of features")
86+
}
87+
}
88+
89+
// NOTE: for now, we just take a single path. By the end of this PR
90+
// series, all paths will be kept.
91+
path := paths[0]
92+
93+
finalHop := path.BlindedPath.
94+
BlindedHops[len(path.BlindedPath.BlindedHops)-1]
95+
96+
return &BlindedPaymentPathSet{
97+
paths: paths,
98+
targetPubKey: finalHop.BlindedNodePub,
99+
features: features,
100+
}, nil
101+
}
102+
103+
// TargetPubKey returns the public key to be used as the destination node's
104+
// public key during pathfinding.
105+
func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
106+
return s.targetPubKey
107+
}
108+
109+
// Features returns the set of relay features available for the payment.
110+
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
111+
return s.features
112+
}
113+
114+
// GetPath is a temporary getter for the single path that the set holds.
115+
// This will be removed later on in this PR.
116+
func (s *BlindedPaymentPathSet) GetPath() *BlindedPayment {
117+
return s.paths[0]
118+
}
119+
120+
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
121+
// largest last-hop payload. This is to be used for onion size estimation in
122+
// path finding.
123+
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
124+
var (
125+
largestPath *BlindedPayment
126+
currentMax int
127+
)
128+
for _, path := range s.paths {
129+
numHops := len(path.BlindedPath.BlindedHops)
130+
lastHop := path.BlindedPath.BlindedHops[numHops-1]
131+
132+
if len(lastHop.CipherText) > currentMax {
133+
largestPath = path
134+
}
135+
}
136+
137+
return largestPath
138+
}
139+
140+
// ToRouteHints converts the blinded path payment set into a RouteHints map so
141+
// that the blinded payment paths can be treated like route hints throughout the
142+
// code base.
143+
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
144+
hints := make(RouteHints)
145+
146+
for _, path := range s.paths {
147+
pathHints, err := path.toRouteHints()
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
for from, edges := range pathHints {
153+
hints[from] = append(hints[from], edges...)
154+
}
155+
}
156+
157+
if len(hints) == 0 {
158+
return nil, nil
159+
}
160+
161+
return hints, nil
162+
}
163+
28164
// BlindedPayment provides the path and payment parameters required to send a
29165
// payment along a blinded path.
30166
type BlindedPayment struct {

rpcserver.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5242,28 +5242,24 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
52425242
payIntent.metadata = payReq.Metadata
52435243

52445244
if len(payReq.BlindedPaymentPaths) > 0 {
5245-
// NOTE: Currently we only choose a single payment path.
5246-
// This will be updated in a future PR to handle
5247-
// multiple blinded payment paths.
5248-
path := payReq.BlindedPaymentPaths[0]
5249-
if len(path.Hops) == 0 {
5250-
return payIntent, fmt.Errorf("a blinded " +
5251-
"payment must have at least 1 hop")
5245+
pathSet, err := routerrpc.BuildBlindedPathSet(
5246+
payReq.BlindedPaymentPaths,
5247+
)
5248+
if err != nil {
5249+
return payIntent, err
52525250
}
5251+
payIntent.blindedPayment = pathSet.GetPath()
52535252

5254-
finalHop := path.Hops[len(path.Hops)-1]
5255-
payIntent.blindedPayment =
5256-
routerrpc.MarshalBlindedPayment(path)
5257-
5258-
// Replace the target node with the blinded public key
5259-
// of the blinded path's final node.
5253+
// Replace the destination node with the target public
5254+
// key of the blinded path set.
52605255
copy(
52615256
payIntent.dest[:],
5262-
finalHop.BlindedNodePub.SerializeCompressed(),
5257+
pathSet.TargetPubKey().SerializeCompressed(),
52635258
)
52645259

5265-
if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() {
5266-
payIntent.destFeatures = path.Features.Clone()
5260+
pathFeatures := pathSet.Features()
5261+
if !pathFeatures.IsEmpty() {
5262+
payIntent.destFeatures = pathFeatures.Clone()
52675263
}
52685264
}
52695265

0 commit comments

Comments
 (0)