• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 12205625987

06 Dec 2024 08:23PM UTC coverage: 49.763% (-0.04%) from 49.807%
12205625987

Pull #9316

github

ziggie1984
docs: add release-notes.
Pull Request #9316: routing: fix mc blinded path behaviour.

165 of 181 new or added lines in 5 files covered. (91.16%)

238 existing lines in 28 files now uncovered.

100104 of 201163 relevant lines covered (49.76%)

2.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.11
/routing/blinding.go
1
package routing
2

3
import (
4
        "bytes"
5
        "errors"
6
        "fmt"
7

8
        "github.com/btcsuite/btcd/btcec/v2"
9
        sphinx "github.com/lightningnetwork/lightning-onion"
10
        "github.com/lightningnetwork/lnd/graph/db/models"
11
        "github.com/lightningnetwork/lnd/input"
12
        "github.com/lightningnetwork/lnd/lnwire"
13
        "github.com/lightningnetwork/lnd/routing/route"
14
)
15

16
// BlindedPathNUMSHex is the hex encoded version of the blinded path target
17
// NUMs key (in compressed format) which has no known private key.
18
// This was generated using the following script:
19
// https://github.com/lightninglabs/lightning-node-connect/tree/master/
20
// mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
21
const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
22
        "dd6e95f5e299dfd54e"
23

24
var (
25
        // ErrNoBlindedPath is returned when the blinded path in a blinded
26
        // payment is missing.
27
        ErrNoBlindedPath = errors.New("blinded path required")
28

29
        // ErrInsufficientBlindedHops is returned when a blinded path does
30
        // not have enough blinded hops.
31
        ErrInsufficientBlindedHops = errors.New("blinded path requires " +
32
                "at least one hop")
33

34
        // ErrHTLCRestrictions is returned when a blinded path has invalid
35
        // HTLC maximum and minimum values.
36
        ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
37

38
        // BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
39
        // has no known private key.
40
        BlindedPathNUMSKey = input.MustParsePubKey(BlindedPathNUMSHex)
41

42
        // CompressedBlindedPathNUMSKey is the compressed version of the
43
        // BlindedPathNUMSKey.
44
        CompressedBlindedPathNUMSKey = BlindedPathNUMSKey.SerializeCompressed()
45
)
46

47
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
48
// blinded paths provided by the recipient of a payment.
49
//
50
// NOTE: for now this only holds a single BlindedPayment. By the end of the PR
51
// series, it will handle multiple paths.
52
type BlindedPaymentPathSet struct {
53
        // paths is the set of blinded payment paths for a single payment.
54
        // NOTE: For now this will always only have a single entry. By the end
55
        // of this PR, it can hold multiple.
56
        paths []*BlindedPayment
57

58
        // targetPubKey is the ephemeral node pub key that we will inject into
59
        // each path as the last hop. This is only for the sake of path finding.
60
        // Once the path has been found, the original destination pub key is
61
        // used again. In the edge case where there is only a single hop in the
62
        // path (the introduction node is the destination node), then this will
63
        // just be the introduction node's real public key.
64
        targetPubKey *btcec.PublicKey
65

66
        // features is the set of relay features available for the payment.
67
        // This is extracted from the set of blinded payment paths. At the
68
        // moment we require that all paths for the same payment have the
69
        // same feature set.
70
        features *lnwire.FeatureVector
71

72
        // finalCLTV is the final hop's expiry delta of _any_ path in the set.
73
        // For any multi-hop path, the final CLTV delta should be seen as zero
74
        // since the final hop's final CLTV delta is accounted for in the
75
        // accumulated path policy values. The only edge case is for when the
76
        // final hop in the path is also the introduction node in which case
77
        // that path's FinalCLTV must be the non-zero min CLTV of the final hop
78
        // so that it is accounted for in path finding. For this reason, if
79
        // we have any single path in the set with only one hop, then we throw
80
        // away all the other paths. This should be fine to do since if there is
81
        // a path where the intro node is also the destination node, then there
82
        // isn't any need to try any other longer blinded path. In other words,
83
        // if this value is non-zero, then there is only one path in this
84
        // blinded path set and that path only has a single hop: the
85
        // introduction node.
86
        finalCLTV uint16
87
}
88

89
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
90
// BlindedPayments. For blinded paths which have more than one single hop a
91
// dummy hop via a NUMS key is appeneded to allow for MPP path finding via
92
// multiple blinded paths.
93
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
94
        error) {
4✔
95

4✔
96
        if len(paths) == 0 {
4✔
97
                return nil, ErrNoBlindedPath
×
98
        }
×
99

100
        // For now, we assert that all the paths have the same set of features.
101
        features := paths[0].Features
4✔
102
        noFeatures := features == nil || features.IsEmpty()
4✔
103
        for i := 1; i < len(paths); i++ {
8✔
104
                noFeats := paths[i].Features == nil ||
4✔
105
                        paths[i].Features.IsEmpty()
4✔
106

4✔
107
                if noFeatures && !noFeats {
4✔
108
                        return nil, fmt.Errorf("all blinded paths must have " +
×
109
                                "the same set of features")
×
110
                }
×
111

112
                if noFeatures {
8✔
113
                        continue
4✔
114
                }
115

116
                if !features.RawFeatureVector.Equals(
×
117
                        paths[i].Features.RawFeatureVector,
×
118
                ) {
×
119

×
120
                        return nil, fmt.Errorf("all blinded paths must have " +
×
121
                                "the same set of features")
×
122
                }
×
123
        }
124

125
        // For blinded paths we use the NUMS key as a target if the blinded
126
        // path has more hops than just the introduction node.
127
        targetPub := &BlindedPathNUMSKey
4✔
128

4✔
129
        var (
4✔
130
                pathSet        = paths
4✔
131
                finalCLTVDelta uint16
4✔
132
        )
4✔
133

4✔
134
        // In case the paths do NOT include a single hop route we appened a
4✔
135
        // dummy hop via a NUMS key to allow for MPP path finding via multiple
4✔
136
        // blinded paths.
4✔
137
        for _, path := range paths {
8✔
138
                pathLength := len(path.BlindedPath.BlindedHops)
4✔
139

4✔
140
                // If any provided blinded path only has a single hop (ie, the
4✔
141
                // destination node is also the introduction node), then we
4✔
142
                // discard all other paths since we know the real pub key of the
4✔
143
                // destination node. We also then set the final CLTV delta to
4✔
144
                // the path's delta since there are no other edge hints that
4✔
145
                // will account for it.
4✔
146
                if pathLength == 1 {
8✔
147
                        pathSet = []*BlindedPayment{path}
4✔
148
                        finalCLTVDelta = path.CltvExpiryDelta
4✔
149
                        targetPub = path.BlindedPath.IntroductionPoint
4✔
150

4✔
151
                        break
4✔
152
                }
153

154
                // This copies the existing blinded hops before appending a new
155
                // one so that we do not accidentally alter the size of the
156
                // passed blinded paths.
157
                //
158
                // NOTE: This is not a deep copy we only want to ensure that
159
                // the input value to this function is not changed.
160
                copiedHops := make([]*sphinx.BlindedHopInfo, pathLength)
4✔
161
                copy(copiedHops, path.BlindedPath.BlindedHops)
4✔
162
                path.BlindedPath.BlindedHops = copiedHops
4✔
163

4✔
164
                path.BlindedPath.BlindedHops = append(
4✔
165
                        path.BlindedPath.BlindedHops,
4✔
166
                        &sphinx.BlindedHopInfo{
4✔
167
                                BlindedNodePub: &BlindedPathNUMSKey,
4✔
168
                        },
4✔
169
                )
4✔
170
        }
171

172
        return &BlindedPaymentPathSet{
4✔
173
                paths:        pathSet,
4✔
174
                targetPubKey: targetPub,
4✔
175
                features:     features,
4✔
176
                finalCLTV:    finalCLTVDelta,
4✔
177
        }, nil
4✔
178
}
179

180
// TargetPubKey returns the public key to be used as the destination node's
181
// public key during pathfinding.
182
func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
4✔
183
        return s.targetPubKey
4✔
184
}
4✔
185

186
// Features returns the set of relay features available for the payment.
187
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
4✔
188
        return s.features
4✔
189
}
4✔
190

191
// IntroNodeOnlyPath can be called if it is expected that the path set only
192
// contains a single payment path which itself only has one hop. It errors if
193
// this is not the case.
194
func (s *BlindedPaymentPathSet) IntroNodeOnlyPath() (*BlindedPayment, error) {
4✔
195
        if len(s.paths) != 1 {
4✔
196
                return nil, fmt.Errorf("expected only a single path in the "+
×
197
                        "blinded payment set, got %d", len(s.paths))
×
198
        }
×
199

200
        if len(s.paths[0].BlindedPath.BlindedHops) > 1 {
4✔
201
                return nil, fmt.Errorf("an intro node only path cannot have " +
×
202
                        "more than one hop")
×
203
        }
×
204

205
        return s.paths[0], nil
4✔
206
}
207

208
// IsIntroNode returns true if the given vertex is an introduction node for one
209
// of the paths in the blinded payment path set.
210
func (s *BlindedPaymentPathSet) IsIntroNode(source route.Vertex) bool {
4✔
211
        for _, path := range s.paths {
8✔
212
                introVertex := route.NewVertex(
4✔
213
                        path.BlindedPath.IntroductionPoint,
4✔
214
                )
4✔
215
                if source == introVertex {
4✔
216
                        return true
×
217
                }
×
218
        }
219

220
        return false
4✔
221
}
222

223
// FinalCLTVDelta is the minimum CLTV delta to use for the final hop on the
224
// route. In most cases this will return zero since the value is accounted for
225
// in the path's accumulated CLTVExpiryDelta. Only in the edge case of the path
226
// set only including a single path which only includes an introduction node
227
// will this return a non-zero value.
228
func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
4✔
229
        return s.finalCLTV
4✔
230
}
4✔
231

232
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
233
// largest last-hop payload. This is to be used for onion size estimation in
234
// path finding. It also returns the cipher text (encrypted data) of the last
235
// hop.
236
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() (*BlindedPayment,
237
        []byte, error) {
4✔
238

4✔
239
        var (
4✔
240
                largestPath   *BlindedPayment
4✔
241
                encryptedData []byte
4✔
242
                currentMax    int
4✔
243
        )
4✔
244

4✔
245
        if len(s.paths) == 0 {
4✔
NEW
246
                return nil, nil, fmt.Errorf("no blinded paths in the set")
×
NEW
247
        }
×
248

249
        largestPath = s.paths[0]
4✔
250

4✔
251
        for _, path := range s.paths {
8✔
252
                numHops := len(path.BlindedPath.BlindedHops)
4✔
253
                lastHopBlindedPk := path.BlindedPath.BlindedHops[numHops-1].
4✔
254
                        BlindedNodePub
4✔
255

4✔
256
                lastHop := path.BlindedPath.BlindedHops[numHops-1]
4✔
257

4✔
258
                // In case the last hop is the NUMS target, the real last hop
4✔
259
                // is the one before the dummy hop.
4✔
260
                if lastHopBlindedPk != nil && IsBlindedRouteNUMSTargetKey(
4✔
261
                        lastHopBlindedPk.SerializeCompressed(),
4✔
262
                ) {
8✔
263

4✔
264
                        if numHops == 1 {
4✔
NEW
265
                                return nil, nil, fmt.Errorf("route consists " +
×
NEW
266
                                        "only of NUMS key as single hop")
×
NEW
267
                        }
×
268
                        lastHop = path.BlindedPath.BlindedHops[numHops-2]
4✔
269
                }
270

271
                if len(lastHop.CipherText) > currentMax {
8✔
272
                        largestPath = path
4✔
273
                        currentMax = len(lastHop.CipherText)
4✔
274
                        encryptedData = lastHop.CipherText
4✔
275
                }
4✔
276
        }
277

278
        return largestPath, encryptedData, nil
4✔
279
}
280

281
// ToRouteHints converts the blinded path payment set into a RouteHints map so
282
// that the blinded payment paths can be treated like route hints throughout the
283
// code base.
284
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
4✔
285
        hints := make(RouteHints)
4✔
286

4✔
287
        for _, path := range s.paths {
8✔
288
                pathHints, err := path.toRouteHints()
4✔
289
                if err != nil {
4✔
290
                        return nil, err
×
291
                }
×
292

293
                for from, edges := range pathHints {
8✔
294
                        hints[from] = append(hints[from], edges...)
4✔
295
                }
4✔
296
        }
297

298
        if len(hints) == 0 {
8✔
299
                return nil, nil
4✔
300
        }
4✔
301

302
        return hints, nil
4✔
303
}
304

305
// IsBlindedRouteNUMSTargetKey returns true if the given public key is the
306
// NUMS key used as a target for blinded path final hops.
307
func IsBlindedRouteNUMSTargetKey(pk []byte) bool {
4✔
308
        return bytes.Equal(pk, CompressedBlindedPathNUMSKey)
4✔
309
}
4✔
310

311
// BlindedPayment provides the path and payment parameters required to send a
312
// payment along a blinded path.
313
type BlindedPayment struct {
314
        // BlindedPath contains the unblinded introduction point and blinded
315
        // hops for the blinded section of the payment.
316
        BlindedPath *sphinx.BlindedPath
317

318
        // BaseFee is the total base fee to be paid for payments made over the
319
        // blinded path.
320
        BaseFee uint32
321

322
        // ProportionalFeeRate is the aggregated proportional fee rate for
323
        // payments made over the blinded path.
324
        ProportionalFeeRate uint32
325

326
        // CltvExpiryDelta is the total expiry delta for the blinded path. This
327
        // field includes the CLTV for the blinded hops *and* the final cltv
328
        // delta for the receiver.
329
        CltvExpiryDelta uint16
330

331
        // HtlcMinimum is the highest HLTC minimum supported along the blinded
332
        // path (while some hops may have lower values, we're effectively
333
        // bounded by the highest minimum).
334
        HtlcMinimum uint64
335

336
        // HtlcMaximum is the lowest HTLC maximum supported along the blinded
337
        // path (while some hops may have higher values, we're effectively
338
        // bounded by the lowest maximum).
339
        HtlcMaximum uint64
340

341
        // Features is the set of relay features available for the payment.
342
        Features *lnwire.FeatureVector
343
}
344

345
// Validate performs validation on a blinded payment.
346
func (b *BlindedPayment) Validate() error {
4✔
347
        if b.BlindedPath == nil {
4✔
348
                return ErrNoBlindedPath
×
349
        }
×
350

351
        // The sphinx library inserts the introduction node as the first hop,
352
        // so we expect at least one hop.
353
        if len(b.BlindedPath.BlindedHops) < 1 {
4✔
354
                return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
×
355
                        len(b.BlindedPath.BlindedHops))
×
356
        }
×
357

358
        if b.HtlcMaximum < b.HtlcMinimum {
4✔
359
                return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions,
×
360
                        b.HtlcMaximum, b.HtlcMinimum)
×
361
        }
×
362

363
        for _, hop := range b.BlindedPath.BlindedHops {
8✔
364
                // The first hop of the blinded path does not necessarily have
4✔
365
                // blinded node pub key because it is the introduction point.
4✔
366
                if hop.BlindedNodePub == nil {
4✔
NEW
367
                        continue
×
368
                }
369

370
                if IsBlindedRouteNUMSTargetKey(
4✔
371
                        hop.BlindedNodePub.SerializeCompressed(),
4✔
372
                ) {
4✔
NEW
373

×
NEW
374
                        return fmt.Errorf("blinded path cannot include NUMS "+
×
NEW
375
                                "key: %s", BlindedPathNUMSHex)
×
NEW
376
                }
×
377
        }
378

379
        return nil
4✔
380
}
381

382
// toRouteHints produces a set of chained route hints that represent a blinded
383
// path. In the case of a single hop blinded route (which is paying directly
384
// to the introduction point), no hints will be returned. In this case callers
385
// *must* account for the blinded route's CLTV delta elsewhere (as this is
386
// effectively the final_cltv_delta for the receiving introduction node). In
387
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
388
// hints (both for intermediate hops and the final_cltv_delta for the receiving
389
// node).
390
func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
4✔
391
        // If we just have a single hop in our blinded route, it just contains
4✔
392
        // an introduction node (this is a valid path according to the spec).
4✔
393
        // Since we have the un-blinded node ID for the introduction node, we
4✔
394
        // don't need to add any route hints.
4✔
395
        if len(b.BlindedPath.BlindedHops) == 1 {
8✔
396
                return nil, nil
4✔
397
        }
4✔
398

399
        hintCount := len(b.BlindedPath.BlindedHops) - 1
4✔
400
        hints := make(
4✔
401
                RouteHints, hintCount,
4✔
402
        )
4✔
403

4✔
404
        // Start at the unblinded introduction node, because our pathfinding
4✔
405
        // will be able to locate this point in the graph.
4✔
406
        fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
4✔
407

4✔
408
        features := lnwire.EmptyFeatureVector()
4✔
409
        if b.Features != nil {
8✔
410
                features = b.Features.Clone()
4✔
411
        }
4✔
412

413
        // Use the total aggregate relay parameters for the entire blinded
414
        // route as the policy for the hint from our introduction node. This
415
        // will ensure that pathfinding provides sufficient fees/delay for the
416
        // blinded portion to the introduction node.
417
        firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
4✔
418
        edgePolicy := &models.CachedEdgePolicy{
4✔
419
                TimeLockDelta: b.CltvExpiryDelta,
4✔
420
                MinHTLC:       lnwire.MilliSatoshi(b.HtlcMinimum),
4✔
421
                MaxHTLC:       lnwire.MilliSatoshi(b.HtlcMaximum),
4✔
422
                FeeBaseMSat:   lnwire.MilliSatoshi(b.BaseFee),
4✔
423
                FeeProportionalMillionths: lnwire.MilliSatoshi(
4✔
424
                        b.ProportionalFeeRate,
4✔
425
                ),
4✔
426
                ToNodePubKey: func() route.Vertex {
8✔
427
                        return route.NewVertex(
4✔
428
                                // The first node in this slice is
4✔
429
                                // the introduction node, so we start
4✔
430
                                // at index 1 to get the first blinded
4✔
431
                                // relaying node.
4✔
432
                                firstBlindedHop,
4✔
433
                        )
4✔
434
                },
4✔
435
                ToNodeFeatures: features,
436
        }
437

438
        lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
4✔
439
        if err != nil {
4✔
440
                return nil, err
×
441
        }
×
442

443
        hints[fromNode] = []AdditionalEdge{lastEdge}
4✔
444

4✔
445
        // Start at an offset of 1 because the first node in our blinded hops
4✔
446
        // is the introduction node and terminate at the second-last node
4✔
447
        // because we're dealing with hops as pairs.
4✔
448
        for i := 1; i < hintCount; i++ {
8✔
449
                // Set our origin node to the current
4✔
450
                fromNode = route.NewVertex(
4✔
451
                        b.BlindedPath.BlindedHops[i].BlindedNodePub,
4✔
452
                )
4✔
453

4✔
454
                // Create a hint which has no fee or cltv delta. We
4✔
455
                // specifically want zero values here because our relay
4✔
456
                // parameters are expressed in encrypted blobs rather than the
4✔
457
                // route itself for blinded routes.
4✔
458
                nextHopIdx := i + 1
4✔
459
                nextNode := route.NewVertex(
4✔
460
                        b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
4✔
461
                )
4✔
462

4✔
463
                edgePolicy := &models.CachedEdgePolicy{
4✔
464
                        ToNodePubKey: func() route.Vertex {
8✔
465
                                return nextNode
4✔
466
                        },
4✔
467
                        ToNodeFeatures: features,
468
                }
469

470
                lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
4✔
471
                if err != nil {
4✔
472
                        return nil, err
×
473
                }
×
474

475
                hints[fromNode] = []AdditionalEdge{lastEdge}
4✔
476
        }
477

478
        return hints, nil
4✔
479
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc