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
11
"github.com/lightningnetwork/lnd/channeldb/models"
10
- "github.com/lightningnetwork/lnd/fn "
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 {
@@ -198,21 +235,33 @@ func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
198
235
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
199
236
// largest last-hop payload. This is to be used for onion size estimation in
200
237
// path finding.
201
- func (s * BlindedPaymentPathSet ) LargestLastHopPayloadPath () * BlindedPayment {
238
+ func (s * BlindedPaymentPathSet ) LargestLastHopPayloadPath () (* BlindedPayment ,
239
+ error ) {
240
+
202
241
var (
203
242
largestPath * BlindedPayment
204
243
currentMax int
205
244
)
245
+
246
+ if len (s .paths ) == 0 {
247
+ return nil , fmt .Errorf ("no blinded paths in the set" )
248
+ }
249
+
250
+ // We set the largest path to make sure we always return a path even
251
+ // if the cipher text is empty.
252
+ largestPath = s .paths [0 ]
253
+
206
254
for _ , path := range s .paths {
207
255
numHops := len (path .BlindedPath .BlindedHops )
208
256
lastHop := path .BlindedPath .BlindedHops [numHops - 1 ]
209
257
210
258
if len (lastHop .CipherText ) > currentMax {
211
259
largestPath = path
260
+ currentMax = len (lastHop .CipherText )
212
261
}
213
262
}
214
263
215
- return largestPath
264
+ return largestPath , nil
216
265
}
217
266
218
267
// ToRouteHints converts the blinded path payment set into a RouteHints map so
@@ -222,7 +271,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
222
271
hints := make (RouteHints )
223
272
224
273
for _ , path := range s .paths {
225
- pathHints , err := path .toRouteHints (fn . Some ( s . targetPubKey ) )
274
+ pathHints , err := path .toRouteHints ()
226
275
if err != nil {
227
276
return nil , err
228
277
}
@@ -239,6 +288,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
239
288
return hints , nil
240
289
}
241
290
291
+ // IsBlindedRouteNUMSTargetKey returns true if the given public key is the
292
+ // NUMS key used as a target for blinded path final hops.
293
+ func IsBlindedRouteNUMSTargetKey (pk []byte ) bool {
294
+ return bytes .Equal (pk , CompressedBlindedPathNUMSKey )
295
+ }
296
+
242
297
// BlindedPayment provides the path and payment parameters required to send a
243
298
// payment along a blinded path.
244
299
type BlindedPayment struct {
@@ -291,6 +346,22 @@ func (b *BlindedPayment) Validate() error {
291
346
b .HtlcMaximum , b .HtlcMinimum )
292
347
}
293
348
349
+ for _ , hop := range b .BlindedPath .BlindedHops {
350
+ // The first hop of the blinded path does not necessarily have
351
+ // blinded node pub key because it is the introduction point.
352
+ if hop .BlindedNodePub == nil {
353
+ continue
354
+ }
355
+
356
+ if IsBlindedRouteNUMSTargetKey (
357
+ hop .BlindedNodePub .SerializeCompressed (),
358
+ ) {
359
+
360
+ return fmt .Errorf ("blinded path cannot include NUMS " +
361
+ "key: %s" , BlindedPathNUMSHex )
362
+ }
363
+ }
364
+
294
365
return nil
295
366
}
296
367
@@ -301,11 +372,8 @@ func (b *BlindedPayment) Validate() error {
301
372
// effectively the final_cltv_delta for the receiving introduction node). In
302
373
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
303
374
// 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
-
375
+ // node).
376
+ func (b * BlindedPayment ) toRouteHints () (RouteHints , error ) {
309
377
// If we just have a single hop in our blinded route, it just contains
310
378
// an introduction node (this is a valid path according to the spec).
311
379
// Since we have the un-blinded node ID for the introduction node, we
@@ -393,16 +461,77 @@ func (b *BlindedPayment) toRouteHints(
393
461
hints [fromNode ] = []AdditionalEdge {lastEdge }
394
462
}
395
463
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 )
464
+ return hints , nil
465
+ }
466
+
467
+ // deepCopy returns a deep copy of the BlindedPayment.
468
+ func (b * BlindedPayment ) deepCopy () * BlindedPayment {
469
+ if b == nil {
470
+ return nil
471
+ }
472
+
473
+ cpyPayment := & BlindedPayment {
474
+ BaseFee : b .BaseFee ,
475
+ ProportionalFeeRate : b .ProportionalFeeRate ,
476
+ CltvExpiryDelta : b .CltvExpiryDelta ,
477
+ HtlcMinimum : b .HtlcMinimum ,
478
+ HtlcMaximum : b .HtlcMaximum ,
479
+ }
480
+
481
+ // Deep copy the BlindedPath if it exists
482
+ if b .BlindedPath != nil {
483
+ cpyPayment .BlindedPath = & sphinx.BlindedPath {
484
+ BlindedHops : make ([]* sphinx.BlindedHopInfo ,
485
+ len (b .BlindedPath .BlindedHops )),
401
486
}
402
487
403
- // Then override the final hint with this updated edge.
404
- hints [fromNode ] = []AdditionalEdge {lastEdge }
405
- })
488
+ if b .BlindedPath .IntroductionPoint != nil {
489
+ cpyPayment .BlindedPath .IntroductionPoint =
490
+ copyPublicKey (b .BlindedPath .IntroductionPoint )
491
+ }
406
492
407
- return hints , nil
493
+ if b .BlindedPath .BlindingPoint != nil {
494
+ cpyPayment .BlindedPath .BlindingPoint =
495
+ copyPublicKey (b .BlindedPath .BlindingPoint )
496
+ }
497
+
498
+ // Copy each blinded hop info.
499
+ for i , hop := range b .BlindedPath .BlindedHops {
500
+ if hop == nil {
501
+ continue
502
+ }
503
+
504
+ cpyHop := & sphinx.BlindedHopInfo {
505
+ CipherText : hop .CipherText ,
506
+ }
507
+
508
+ if hop .BlindedNodePub != nil {
509
+ cpyHop .BlindedNodePub =
510
+ copyPublicKey (hop .BlindedNodePub )
511
+ }
512
+
513
+ cpyHop .CipherText = make ([]byte , len (hop .CipherText ))
514
+ copy (cpyHop .CipherText , hop .CipherText )
515
+
516
+ cpyPayment .BlindedPath .BlindedHops [i ] = cpyHop
517
+ }
518
+ }
519
+
520
+ // Deep copy the Features if they exist
521
+ if b .Features != nil {
522
+ cpyPayment .Features = b .Features .Clone ()
523
+ }
524
+
525
+ return cpyPayment
526
+ }
527
+
528
+ // copyPublicKey makes a deep copy of a public key.
529
+ //
530
+ // TODO(ziggie): Remove this function if this is available in the btcec library.
531
+ func copyPublicKey (pk * btcec.PublicKey ) * btcec.PublicKey {
532
+ var result secp256k1.JacobianPoint
533
+ pk .AsJacobian (& result )
534
+ result .ToAffine ()
535
+
536
+ return btcec .NewPublicKey (& result .X , & result .Y )
408
537
}
0 commit comments