11package routing
22
33import (
4+ "bytes"
45 "errors"
56 "fmt"
67
78 "github.com/btcsuite/btcd/btcec/v2"
9+ "github.com/decred/dcrd/dcrec/secp256k1/v4"
810 sphinx "github.com/lightningnetwork/lightning-onion"
9- "github.com/lightningnetwork/lnd/fn/v2"
1011 "github.com/lightningnetwork/lnd/graph/db/models"
12+ "github.com/lightningnetwork/lnd/input"
1113 "github.com/lightningnetwork/lnd/lnwire"
1214 "github.com/lightningnetwork/lnd/routing/route"
1315)
1416
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+
1525var (
1626 // ErrNoBlindedPath is returned when the blinded path in a blinded
1727 // payment is missing.
2535 // ErrHTLCRestrictions is returned when a blinded path has invalid
2636 // HTLC maximum and minimum values.
2737 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 ()
2846)
2947
3048// BlindedPaymentPathSet groups the data we need to handle sending to a set of
@@ -70,7 +88,9 @@ type BlindedPaymentPathSet struct {
7088}
7189
7290// 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.
7494func NewBlindedPaymentPathSet (paths []* BlindedPayment ) (* BlindedPaymentPathSet ,
7595 error ) {
7696
@@ -103,36 +123,53 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
103123 }
104124 }
105125
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 ()
111130 }
112- targetPub := targetPriv .PubKey ()
113131
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
129160 }
130161
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+ )
136173 }
137174
138175 return & BlindedPaymentPathSet {
@@ -198,21 +235,33 @@ func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
198235// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
199236// largest last-hop payload. This is to be used for onion size estimation in
200237// path finding.
201- func (s * BlindedPaymentPathSet ) LargestLastHopPayloadPath () * BlindedPayment {
238+ func (s * BlindedPaymentPathSet ) LargestLastHopPayloadPath () (* BlindedPayment ,
239+ error ) {
240+
202241 var (
203242 largestPath * BlindedPayment
204243 currentMax int
205244 )
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+
206254 for _ , path := range s .paths {
207255 numHops := len (path .BlindedPath .BlindedHops )
208256 lastHop := path .BlindedPath .BlindedHops [numHops - 1 ]
209257
210258 if len (lastHop .CipherText ) > currentMax {
211259 largestPath = path
260+ currentMax = len (lastHop .CipherText )
212261 }
213262 }
214263
215- return largestPath
264+ return largestPath , nil
216265}
217266
218267// ToRouteHints converts the blinded path payment set into a RouteHints map so
@@ -222,7 +271,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
222271 hints := make (RouteHints )
223272
224273 for _ , path := range s .paths {
225- pathHints , err := path .toRouteHints (fn . Some ( s . targetPubKey ) )
274+ pathHints , err := path .toRouteHints ()
226275 if err != nil {
227276 return nil , err
228277 }
@@ -239,6 +288,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
239288 return hints , nil
240289}
241290
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+
242297// BlindedPayment provides the path and payment parameters required to send a
243298// payment along a blinded path.
244299type BlindedPayment struct {
@@ -291,6 +346,22 @@ func (b *BlindedPayment) Validate() error {
291346 b .HtlcMaximum , b .HtlcMinimum )
292347 }
293348
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+
294365 return nil
295366}
296367
@@ -301,11 +372,8 @@ func (b *BlindedPayment) Validate() error {
301372// effectively the final_cltv_delta for the receiving introduction node). In
302373// the case of multiple blinded hops, CLTV delta is fully accounted for in the
303374// 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 ) {
309377 // If we just have a single hop in our blinded route, it just contains
310378 // an introduction node (this is a valid path according to the spec).
311379 // Since we have the un-blinded node ID for the introduction node, we
@@ -393,16 +461,77 @@ func (b *BlindedPayment) toRouteHints(
393461 hints [fromNode ] = []AdditionalEdge {lastEdge }
394462 }
395463
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 )),
401486 }
402487
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+ }
406492
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 )
408537}
0 commit comments