1
1
package routing
2
2
3
3
import (
4
+ "bytes"
4
5
"errors"
5
6
"fmt"
6
7
7
8
"github.com/btcsuite/btcd/btcec/v2"
9
+ "github.com/decred/dcrd/dcrec/secp256k1/v4"
8
10
sphinx "github.com/lightningnetwork/lightning-onion"
9
- "github.com/lightningnetwork/lnd/fn/v2"
10
11
"github.com/lightningnetwork/lnd/graph/db/models"
12
+ "github.com/lightningnetwork/lnd/input"
11
13
"github.com/lightningnetwork/lnd/lnwire"
12
14
"github.com/lightningnetwork/lnd/routing/route"
13
15
)
14
16
17
+ // BlindedPathNUMSHex is the hex encoded version of the blinded path target
18
+ // NUMs key (in compressed format) which has no known private key.
19
+ // This was generated using the following script:
20
+ // https://github.com/lightninglabs/lightning-node-connect/tree/master/
21
+ // mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
22
+ const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
23
+ "dd6e95f5e299dfd54e"
24
+
15
25
var (
16
26
// ErrNoBlindedPath is returned when the blinded path in a blinded
17
27
// payment is missing.
25
35
// ErrHTLCRestrictions is returned when a blinded path has invalid
26
36
// HTLC maximum and minimum values.
27
37
ErrHTLCRestrictions = errors .New ("invalid htlc minimum and maximum" )
38
+
39
+ // BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
40
+ // has no known private key.
41
+ BlindedPathNUMSKey = input .MustParsePubKey (BlindedPathNUMSHex )
42
+
43
+ // CompressedBlindedPathNUMSKey is the compressed version of the
44
+ // BlindedPathNUMSKey.
45
+ CompressedBlindedPathNUMSKey = BlindedPathNUMSKey .SerializeCompressed ()
28
46
)
29
47
30
48
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
@@ -70,7 +88,9 @@ type BlindedPaymentPathSet struct {
70
88
}
71
89
72
90
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
73
- // BlindedPayments.
91
+ // BlindedPayments. For blinded paths which have more than one single hop a
92
+ // dummy hop via a NUMS key is appeneded to allow for MPP path finding via
93
+ // multiple blinded paths.
74
94
func NewBlindedPaymentPathSet (paths []* BlindedPayment ) (* BlindedPaymentPathSet ,
75
95
error ) {
76
96
@@ -103,36 +123,53 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
103
123
}
104
124
}
105
125
106
- // Derive an ephemeral target priv key that will be injected into each
107
- // blinded path final hop.
108
- targetPriv , err := btcec .NewPrivateKey ()
109
- if err != nil {
110
- return nil , err
126
+ // Deep copy the paths to avoid mutating the original paths.
127
+ pathSet := make ([]* BlindedPayment , len (paths ))
128
+ for i , path := range paths {
129
+ pathSet [i ] = path .deepCopy ()
111
130
}
112
- targetPub := targetPriv .PubKey ()
113
131
114
- var (
115
- pathSet = paths
116
- finalCLTVDelta uint16
117
- )
118
- // If any provided blinded path only has a single hop (ie, the
119
- // destination node is also the introduction node), then we discard all
120
- // other paths since we know the real pub key of the destination node.
121
- // We also then set the final CLTV delta to the path's delta since
122
- // there are no other edge hints that will account for it. For a single
123
- // hop path, there is also no need for the pseudo target pub key
124
- // replacement, so our target pub key in this case just remains the
125
- // real introduction node ID.
126
- for _ , path := range paths {
127
- if len (path .BlindedPath .BlindedHops ) != 1 {
128
- continue
132
+ // For blinded paths we use the NUMS key as a target if the blinded
133
+ // path has more hops than just the introduction node.
134
+ targetPub := & BlindedPathNUMSKey
135
+
136
+ var finalCLTVDelta uint16
137
+
138
+ // In case the paths do NOT include a single hop route we append a
139
+ // dummy hop via a NUMS key to allow for MPP path finding via multiple
140
+ // blinded paths. A unified target is needed to use all blinded paths
141
+ // during the payment lifecycle. A dummy hop is solely added for the
142
+ // path finding process and is removed after the path is found. This
143
+ // ensures that we still populate the mission control with the correct
144
+ // data and also respect these mc entries when looking for a path.
145
+ for _ , path := range pathSet {
146
+ pathLength := len (path .BlindedPath .BlindedHops )
147
+
148
+ // If any provided blinded path only has a single hop (ie, the
149
+ // destination node is also the introduction node), then we
150
+ // discard all other paths since we know the real pub key of the
151
+ // destination node. We also then set the final CLTV delta to
152
+ // the path's delta since there are no other edge hints that
153
+ // will account for it.
154
+ if pathLength == 1 {
155
+ pathSet = []* BlindedPayment {path }
156
+ finalCLTVDelta = path .CltvExpiryDelta
157
+ targetPub = path .BlindedPath .IntroductionPoint
158
+
159
+ break
129
160
}
130
161
131
- pathSet = []* BlindedPayment {path }
132
- finalCLTVDelta = path .CltvExpiryDelta
133
- targetPub = path .BlindedPath .IntroductionPoint
134
-
135
- break
162
+ lastHop := path .BlindedPath .BlindedHops [pathLength - 1 ]
163
+ path .BlindedPath .BlindedHops = append (
164
+ path .BlindedPath .BlindedHops ,
165
+ & sphinx.BlindedHopInfo {
166
+ BlindedNodePub : & BlindedPathNUMSKey ,
167
+ // We add the last hop's cipher text so that
168
+ // the payload size of the final hop is equal
169
+ // to the real last hop.
170
+ CipherText : lastHop .CipherText ,
171
+ },
172
+ )
136
173
}
137
174
138
175
return & BlindedPaymentPathSet {
@@ -222,7 +259,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
222
259
hints := make (RouteHints )
223
260
224
261
for _ , path := range s .paths {
225
- pathHints , err := path .toRouteHints (fn . Some ( s . targetPubKey ) )
262
+ pathHints , err := path .toRouteHints ()
226
263
if err != nil {
227
264
return nil , err
228
265
}
@@ -239,6 +276,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
239
276
return hints , nil
240
277
}
241
278
279
+ // IsBlindedRouteNUMSTargetKey returns true if the given public key is the
280
+ // NUMS key used as a target for blinded path final hops.
281
+ func IsBlindedRouteNUMSTargetKey (pk []byte ) bool {
282
+ return bytes .Equal (pk , CompressedBlindedPathNUMSKey )
283
+ }
284
+
242
285
// BlindedPayment provides the path and payment parameters required to send a
243
286
// payment along a blinded path.
244
287
type BlindedPayment struct {
@@ -291,6 +334,22 @@ func (b *BlindedPayment) Validate() error {
291
334
b .HtlcMaximum , b .HtlcMinimum )
292
335
}
293
336
337
+ for _ , hop := range b .BlindedPath .BlindedHops {
338
+ // The first hop of the blinded path does not necessarily have
339
+ // blinded node pub key because it is the introduction point.
340
+ if hop .BlindedNodePub == nil {
341
+ continue
342
+ }
343
+
344
+ if IsBlindedRouteNUMSTargetKey (
345
+ hop .BlindedNodePub .SerializeCompressed (),
346
+ ) {
347
+
348
+ return fmt .Errorf ("blinded path cannot include NUMS " +
349
+ "key: %s" , BlindedPathNUMSHex )
350
+ }
351
+ }
352
+
294
353
return nil
295
354
}
296
355
@@ -301,11 +360,8 @@ func (b *BlindedPayment) Validate() error {
301
360
// effectively the final_cltv_delta for the receiving introduction node). In
302
361
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
303
362
// hints (both for intermediate hops and the final_cltv_delta for the receiving
304
- // node). The pseudoTarget, if provided, will be used to override the pub key
305
- // of the destination node in the path.
306
- func (b * BlindedPayment ) toRouteHints (
307
- pseudoTarget fn.Option [* btcec.PublicKey ]) (RouteHints , error ) {
308
-
363
+ // node).
364
+ func (b * BlindedPayment ) toRouteHints () (RouteHints , error ) {
309
365
// If we just have a single hop in our blinded route, it just contains
310
366
// an introduction node (this is a valid path according to the spec).
311
367
// Since we have the un-blinded node ID for the introduction node, we
@@ -393,16 +449,77 @@ func (b *BlindedPayment) toRouteHints(
393
449
hints [fromNode ] = []AdditionalEdge {lastEdge }
394
450
}
395
451
396
- pseudoTarget .WhenSome (func (key * btcec.PublicKey ) {
397
- // For the very last hop on the path, switch out the ToNodePub
398
- // for the pseudo target pub key.
399
- lastEdge .policy .ToNodePubKey = func () route.Vertex {
400
- return route .NewVertex (key )
452
+ return hints , nil
453
+ }
454
+
455
+ // deepCopy returns a deep copy of the BlindedPayment.
456
+ func (b * BlindedPayment ) deepCopy () * BlindedPayment {
457
+ if b == nil {
458
+ return nil
459
+ }
460
+
461
+ cpyPayment := & BlindedPayment {
462
+ BaseFee : b .BaseFee ,
463
+ ProportionalFeeRate : b .ProportionalFeeRate ,
464
+ CltvExpiryDelta : b .CltvExpiryDelta ,
465
+ HtlcMinimum : b .HtlcMinimum ,
466
+ HtlcMaximum : b .HtlcMaximum ,
467
+ }
468
+
469
+ // Deep copy the BlindedPath if it exists
470
+ if b .BlindedPath != nil {
471
+ cpyPayment .BlindedPath = & sphinx.BlindedPath {
472
+ BlindedHops : make ([]* sphinx.BlindedHopInfo ,
473
+ len (b .BlindedPath .BlindedHops )),
401
474
}
402
475
403
- // Then override the final hint with this updated edge.
404
- hints [fromNode ] = []AdditionalEdge {lastEdge }
405
- })
476
+ if b .BlindedPath .IntroductionPoint != nil {
477
+ cpyPayment .BlindedPath .IntroductionPoint =
478
+ copyPublicKey (b .BlindedPath .IntroductionPoint )
479
+ }
406
480
407
- return hints , nil
481
+ if b .BlindedPath .BlindingPoint != nil {
482
+ cpyPayment .BlindedPath .BlindingPoint =
483
+ copyPublicKey (b .BlindedPath .BlindingPoint )
484
+ }
485
+
486
+ // Copy each blinded hop info.
487
+ for i , hop := range b .BlindedPath .BlindedHops {
488
+ if hop == nil {
489
+ continue
490
+ }
491
+
492
+ cpyHop := & sphinx.BlindedHopInfo {
493
+ CipherText : hop .CipherText ,
494
+ }
495
+
496
+ if hop .BlindedNodePub != nil {
497
+ cpyHop .BlindedNodePub =
498
+ copyPublicKey (hop .BlindedNodePub )
499
+ }
500
+
501
+ cpyHop .CipherText = make ([]byte , len (hop .CipherText ))
502
+ copy (cpyHop .CipherText , hop .CipherText )
503
+
504
+ cpyPayment .BlindedPath .BlindedHops [i ] = cpyHop
505
+ }
506
+ }
507
+
508
+ // Deep copy the Features if they exist
509
+ if b .Features != nil {
510
+ cpyPayment .Features = b .Features .Clone ()
511
+ }
512
+
513
+ return cpyPayment
514
+ }
515
+
516
+ // copyPublicKey makes a deep copy of a public key.
517
+ //
518
+ // TODO(ziggie): Remove this function if this is available in the btcec library.
519
+ func copyPublicKey (pk * btcec.PublicKey ) * btcec.PublicKey {
520
+ var result secp256k1.JacobianPoint
521
+ pk .AsJacobian (& result )
522
+ result .ToAffine ()
523
+
524
+ return btcec .NewPublicKey (& result .X , & result .Y )
408
525
}
0 commit comments