Skip to content

Commit e47024b

Browse files
committed
routing: Use NUMS point for blinded paths
To be able to do MPP payment to multiple blinded routes we need to add a constant dummy hop as a final hop to every blined path. This is used when sending or querying a blinded path, to let the pathfinder be able to send MPP payments over different blinded routes. For this dummy final hop we use a NUMS key so that we are sure no other node can use this blinded pubkey either in a normal or blinded route. Moreover this helps us handling the mission control data for blinded paths correctly because we always consider the blinded pubkey pairs which are registered with mission control when a payment to a blinded path fails.
1 parent df30b48 commit e47024b

File tree

4 files changed

+257
-58
lines changed

4 files changed

+257
-58
lines changed

routing/blinding.go

Lines changed: 160 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package routing
22

33
import (
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+
1525
var (
1626
// ErrNoBlindedPath is returned when the blinded path in a blinded
1727
// payment is missing.
@@ -25,6 +35,14 @@ var (
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.
7494
func 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{
@@ -222,7 +259,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
222259
hints := make(RouteHints)
223260

224261
for _, path := range s.paths {
225-
pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
262+
pathHints, err := path.toRouteHints()
226263
if err != nil {
227264
return nil, err
228265
}
@@ -239,6 +276,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
239276
return hints, nil
240277
}
241278

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+
242285
// BlindedPayment provides the path and payment parameters required to send a
243286
// payment along a blinded path.
244287
type BlindedPayment struct {
@@ -291,6 +334,22 @@ func (b *BlindedPayment) Validate() error {
291334
b.HtlcMaximum, b.HtlcMinimum)
292335
}
293336

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+
294353
return nil
295354
}
296355

@@ -301,11 +360,8 @@ func (b *BlindedPayment) Validate() error {
301360
// effectively the final_cltv_delta for the receiving introduction node). In
302361
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
303362
// 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) {
309365
// If we just have a single hop in our blinded route, it just contains
310366
// an introduction node (this is a valid path according to the spec).
311367
// Since we have the un-blinded node ID for the introduction node, we
@@ -393,16 +449,77 @@ func (b *BlindedPayment) toRouteHints(
393449
hints[fromNode] = []AdditionalEdge{lastEdge}
394450
}
395451

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)),
401474
}
402475

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+
}
406480

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)
408525
}

routing/blinding_test.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package routing
22

33
import (
44
"bytes"
5+
"reflect"
56
"testing"
67

78
"github.com/btcsuite/btcd/btcec/v2"
89
sphinx "github.com/lightningnetwork/lightning-onion"
9-
"github.com/lightningnetwork/lnd/fn/v2"
1010
"github.com/lightningnetwork/lnd/graph/db/models"
1111
"github.com/lightningnetwork/lnd/lnwire"
1212
"github.com/lightningnetwork/lnd/routing/route"
@@ -129,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
129129
HtlcMaximum: htlcMax,
130130
Features: features,
131131
}
132-
hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
132+
hints, err := blindedPayment.toRouteHints()
133133
require.NoError(t, err)
134134
require.Nil(t, hints)
135135

@@ -184,7 +184,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
184184
},
185185
}
186186

187-
actual, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
187+
actual, err := blindedPayment.toRouteHints()
188188
require.NoError(t, err)
189189

190190
require.Equal(t, len(expected), len(actual))
@@ -218,3 +218,63 @@ func TestBlindedPaymentToHints(t *testing.T) {
218218
require.Equal(t, expectedHint[0], actualHint[0])
219219
}
220220
}
221+
222+
// TestBlindedPaymentDeepCopy tests the deep copy method of the BLindedPayment
223+
// struct.
224+
//
225+
// TODO(ziggie): Make this a property test instead.
226+
func TestBlindedPaymentDeepCopy(t *testing.T) {
227+
_, pkBlind1 := btcec.PrivKeyFromBytes([]byte{1})
228+
_, blindingPoint := btcec.PrivKeyFromBytes([]byte{2})
229+
_, pkBlind2 := btcec.PrivKeyFromBytes([]byte{3})
230+
231+
// Create a test BlindedPayment with non-nil fields
232+
original := &BlindedPayment{
233+
BaseFee: 1000,
234+
ProportionalFeeRate: 2000,
235+
CltvExpiryDelta: 144,
236+
HtlcMinimum: 1000,
237+
HtlcMaximum: 1000000,
238+
Features: lnwire.NewFeatureVector(nil, nil),
239+
BlindedPath: &sphinx.BlindedPath{
240+
IntroductionPoint: pkBlind1,
241+
BlindingPoint: blindingPoint,
242+
BlindedHops: []*sphinx.BlindedHopInfo{
243+
{
244+
BlindedNodePub: pkBlind2,
245+
CipherText: []byte("test cipher"),
246+
},
247+
},
248+
},
249+
}
250+
251+
// Make a deep copy
252+
cpyPayment := original.deepCopy()
253+
254+
// Test 1: Verify the copy is not the same pointer
255+
if cpyPayment == original {
256+
t.Fatal("deepCopy returned same pointer")
257+
}
258+
259+
// Verify all fields are equal
260+
if !reflect.DeepEqual(original, cpyPayment) {
261+
t.Fatal("copy is not equal to original")
262+
}
263+
264+
// Modify the copy and verify it doesn't affect the original
265+
cpyPayment.BaseFee = 2000
266+
cpyPayment.BlindedPath.BlindedHops[0].CipherText = []byte("modified")
267+
268+
require.NotEqual(t, original.BaseFee, cpyPayment.BaseFee)
269+
270+
require.NotEqual(
271+
t,
272+
original.BlindedPath.BlindedHops[0].CipherText,
273+
cpyPayment.BlindedPath.BlindedHops[0].CipherText,
274+
)
275+
276+
// Verify nil handling.
277+
var nilPayment *BlindedPayment
278+
nilCopy := nilPayment.deepCopy()
279+
require.Nil(t, nilCopy)
280+
}

0 commit comments

Comments
 (0)