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

lightningnetwork / lnd / 12168656289

04 Dec 2024 09:30PM UTC coverage: 49.3% (-9.5%) from 58.789%
12168656289

Pull #9316

github

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

75 of 79 new or added lines in 4 files covered. (94.94%)

26127 existing lines in 423 files now uncovered.

99116 of 201048 relevant lines covered (49.3%)

1.56 hits per line

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

82.81
/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

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

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

62
        // features is the set of relay features available for the payment.
63
        // This is extracted from the set of blinded payment paths. At the
64
        // moment we require that all paths for the same payment have the
65
        // same feature set.
66
        features *lnwire.FeatureVector
67

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

85
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
86
// BlindedPayments.
87
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
88
        error) {
3✔
89

3✔
90
        if len(paths) == 0 {
3✔
91
                return nil, ErrNoBlindedPath
×
92
        }
×
93

94
        // For now, we assert that all the paths have the same set of features.
95
        features := paths[0].Features
3✔
96
        noFeatures := features == nil || features.IsEmpty()
3✔
97
        for i := 1; i < len(paths); i++ {
6✔
98
                noFeats := paths[i].Features == nil ||
3✔
99
                        paths[i].Features.IsEmpty()
3✔
100

3✔
101
                if noFeatures && !noFeats {
3✔
102
                        return nil, fmt.Errorf("all blinded paths must have " +
×
103
                                "the same set of features")
×
104
                }
×
105

106
                if noFeatures {
6✔
107
                        continue
3✔
108
                }
109

110
                if !features.RawFeatureVector.Equals(
×
111
                        paths[i].Features.RawFeatureVector,
×
112
                ) {
×
113

×
114
                        return nil, fmt.Errorf("all blinded paths must have " +
×
115
                                "the same set of features")
×
116
                }
×
117
        }
118

119
        // For blinded paths we use the NUMS key as a target if the blinded
120
        // path has more hops than just the introduction node.
121
        targetPub := &BlindedPathNUMSKey
3✔
122

3✔
123
        var (
3✔
124
                pathSet        = paths
3✔
125
                finalCLTVDelta uint16
3✔
126
        )
3✔
127
        // If any provided blinded path only has a single hop (ie, the
3✔
128
        // destination node is also the introduction node), then we discard all
3✔
129
        // other paths since we know the real pub key of the destination node.
3✔
130
        // We also then set the final CLTV delta to the path's delta since
3✔
131
        // there are no other edge hints that will account for it. For a single
3✔
132
        // hop path, there is also no need for the pseudo target pub key
3✔
133
        // replacement, so our target pub key in this case just remains the
3✔
134
        // real introduction node ID.
3✔
135
        for _, path := range paths {
6✔
136
                pathLength := len(path.BlindedPath.BlindedHops)
3✔
137

3✔
138
                if pathLength == 1 {
6✔
139
                        pathSet = []*BlindedPayment{path}
3✔
140
                        finalCLTVDelta = path.CltvExpiryDelta
3✔
141
                        targetPub = path.BlindedPath.IntroductionPoint
3✔
142

3✔
143
                        break
3✔
144
                }
145

146
                // We add a dummy hop to the end of the path so that
147
                // we can do MPP path finding taking all the blinded
148
                // routes into account. Moreover we need to use a
149
                // dummy hop here because we still want to be able to
150
                // penalize the last hop in case the payment fails.
151
                // We need to set the cipher text because it is used for the
152
                // payload size estimation.
153
                lastHopCipherText := path.BlindedPath.BlindedHops[pathLength-1].
3✔
154
                        CipherText
3✔
155

3✔
156
                // Copy the existing BlindedHops before appending a new element.
3✔
157
                copiedHops := make([]*sphinx.BlindedHopInfo, pathLength)
3✔
158
                copy(copiedHops, path.BlindedPath.BlindedHops)
3✔
159
                path.BlindedPath.BlindedHops = copiedHops
3✔
160

3✔
161
                path.BlindedPath.BlindedHops = append(
3✔
162
                        path.BlindedPath.BlindedHops,
3✔
163
                        &sphinx.BlindedHopInfo{
3✔
164
                                BlindedNodePub: &BlindedPathNUMSKey,
3✔
165
                                CipherText:     lastHopCipherText,
3✔
166
                        },
3✔
167
                )
3✔
168
        }
169

170
        return &BlindedPaymentPathSet{
3✔
171
                paths:        pathSet,
3✔
172
                targetPubKey: targetPub,
3✔
173
                features:     features,
3✔
174
                finalCLTV:    finalCLTVDelta,
3✔
175
        }, nil
3✔
176
}
177

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

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

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

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

203
        return s.paths[0], nil
3✔
204
}
205

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

218
        return false
3✔
219
}
220

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

230
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
231
// largest last-hop payload. This is to be used for onion size estimation in
232
// path finding.
233
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
3✔
234
        var (
3✔
235
                largestPath *BlindedPayment
3✔
236
                currentMax  int
3✔
237
        )
3✔
238
        for _, path := range s.paths {
6✔
239
                numHops := len(path.BlindedPath.BlindedHops)
3✔
240
                lastHop := path.BlindedPath.BlindedHops[numHops-1]
3✔
241

3✔
242
                if len(lastHop.CipherText) > currentMax {
6✔
243
                        largestPath = path
3✔
244
                }
3✔
245
        }
246

247
        return largestPath
3✔
248
}
249

250
// ToRouteHints converts the blinded path payment set into a RouteHints map so
251
// that the blinded payment paths can be treated like route hints throughout the
252
// code base.
253
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
3✔
254
        hints := make(RouteHints)
3✔
255

3✔
256
        for _, path := range s.paths {
6✔
257
                pathHints, err := path.toRouteHints()
3✔
258
                if err != nil {
3✔
259
                        return nil, err
×
260
                }
×
261

262
                for from, edges := range pathHints {
6✔
263
                        hints[from] = append(hints[from], edges...)
3✔
264
                }
3✔
265
        }
266

267
        if len(hints) == 0 {
6✔
268
                return nil, nil
3✔
269
        }
3✔
270

271
        return hints, nil
3✔
272
}
273

274
// isBlindedRouteNUMSTargetKey returns true if the given public key is the
275
// NUMS key used as a target for blinded path final hops.
276
func isBlindedRouteNUMSTargetKey(pk []byte) bool {
3✔
277
        return bytes.Equal(pk, BlindedPathNUMSKey.SerializeCompressed())
3✔
278
}
3✔
279

280
// BlindedPayment provides the path and payment parameters required to send a
281
// payment along a blinded path.
282
type BlindedPayment struct {
283
        // BlindedPath contains the unblinded introduction point and blinded
284
        // hops for the blinded section of the payment.
285
        BlindedPath *sphinx.BlindedPath
286

287
        // BaseFee is the total base fee to be paid for payments made over the
288
        // blinded path.
289
        BaseFee uint32
290

291
        // ProportionalFeeRate is the aggregated proportional fee rate for
292
        // payments made over the blinded path.
293
        ProportionalFeeRate uint32
294

295
        // CltvExpiryDelta is the total expiry delta for the blinded path. This
296
        // field includes the CLTV for the blinded hops *and* the final cltv
297
        // delta for the receiver.
298
        CltvExpiryDelta uint16
299

300
        // HtlcMinimum is the highest HLTC minimum supported along the blinded
301
        // path (while some hops may have lower values, we're effectively
302
        // bounded by the highest minimum).
303
        HtlcMinimum uint64
304

305
        // HtlcMaximum is the lowest HTLC maximum supported along the blinded
306
        // path (while some hops may have higher values, we're effectively
307
        // bounded by the lowest maximum).
308
        HtlcMaximum uint64
309

310
        // Features is the set of relay features available for the payment.
311
        Features *lnwire.FeatureVector
312
}
313

314
// Validate performs validation on a blinded payment.
315
func (b *BlindedPayment) Validate() error {
3✔
316
        if b.BlindedPath == nil {
3✔
UNCOV
317
                return ErrNoBlindedPath
×
UNCOV
318
        }
×
319

320
        // The sphinx library inserts the introduction node as the first hop,
321
        // so we expect at least one hop.
322
        if len(b.BlindedPath.BlindedHops) < 1 {
3✔
UNCOV
323
                return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
×
UNCOV
324
                        len(b.BlindedPath.BlindedHops))
×
UNCOV
325
        }
×
326

327
        if b.HtlcMaximum < b.HtlcMinimum {
3✔
UNCOV
328
                return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions,
×
UNCOV
329
                        b.HtlcMaximum, b.HtlcMinimum)
×
UNCOV
330
        }
×
331

332
        for _, hop := range b.BlindedPath.BlindedHops {
6✔
333
                if isBlindedRouteNUMSTargetKey(
3✔
334
                        hop.BlindedNodePub.SerializeCompressed(),
3✔
335
                ) {
3✔
NEW
336

×
NEW
337
                        return fmt.Errorf("blinded path cannot include NUMS "+
×
NEW
338
                                "key: %s", BlindedPathNUMSHex)
×
NEW
339
                }
×
340
        }
341

342
        return nil
3✔
343
}
344

345
// toRouteHints produces a set of chained route hints that represent a blinded
346
// path. In the case of a single hop blinded route (which is paying directly
347
// to the introduction point), no hints will be returned. In this case callers
348
// *must* account for the blinded route's CLTV delta elsewhere (as this is
349
// effectively the final_cltv_delta for the receiving introduction node). In
350
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
351
// hints (both for intermediate hops and the final_cltv_delta for the receiving
352
// node). The pseudoTarget, if provided,  will be used to override the pub key
353
// of the destination node in the path.
354
func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
3✔
355
        // If we just have a single hop in our blinded route, it just contains
3✔
356
        // an introduction node (this is a valid path according to the spec).
3✔
357
        // Since we have the un-blinded node ID for the introduction node, we
3✔
358
        // don't need to add any route hints.
3✔
359
        if len(b.BlindedPath.BlindedHops) == 1 {
6✔
360
                return nil, nil
3✔
361
        }
3✔
362

363
        hintCount := len(b.BlindedPath.BlindedHops) - 1
3✔
364
        hints := make(
3✔
365
                RouteHints, hintCount,
3✔
366
        )
3✔
367

3✔
368
        // Start at the unblinded introduction node, because our pathfinding
3✔
369
        // will be able to locate this point in the graph.
3✔
370
        fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
3✔
371

3✔
372
        features := lnwire.EmptyFeatureVector()
3✔
373
        if b.Features != nil {
6✔
374
                features = b.Features.Clone()
3✔
375
        }
3✔
376

377
        // Use the total aggregate relay parameters for the entire blinded
378
        // route as the policy for the hint from our introduction node. This
379
        // will ensure that pathfinding provides sufficient fees/delay for the
380
        // blinded portion to the introduction node.
381
        firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
3✔
382
        edgePolicy := &models.CachedEdgePolicy{
3✔
383
                TimeLockDelta: b.CltvExpiryDelta,
3✔
384
                MinHTLC:       lnwire.MilliSatoshi(b.HtlcMinimum),
3✔
385
                MaxHTLC:       lnwire.MilliSatoshi(b.HtlcMaximum),
3✔
386
                FeeBaseMSat:   lnwire.MilliSatoshi(b.BaseFee),
3✔
387
                FeeProportionalMillionths: lnwire.MilliSatoshi(
3✔
388
                        b.ProportionalFeeRate,
3✔
389
                ),
3✔
390
                ToNodePubKey: func() route.Vertex {
6✔
391
                        return route.NewVertex(
3✔
392
                                // The first node in this slice is
3✔
393
                                // the introduction node, so we start
3✔
394
                                // at index 1 to get the first blinded
3✔
395
                                // relaying node.
3✔
396
                                firstBlindedHop,
3✔
397
                        )
3✔
398
                },
3✔
399
                ToNodeFeatures: features,
400
        }
401

402
        lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
3✔
403
        if err != nil {
3✔
404
                return nil, err
×
405
        }
×
406

407
        hints[fromNode] = []AdditionalEdge{lastEdge}
3✔
408

3✔
409
        // Start at an offset of 1 because the first node in our blinded hops
3✔
410
        // is the introduction node and terminate at the second-last node
3✔
411
        // because we're dealing with hops as pairs.
3✔
412
        for i := 1; i < hintCount; i++ {
6✔
413
                // Set our origin node to the current
3✔
414
                fromNode = route.NewVertex(
3✔
415
                        b.BlindedPath.BlindedHops[i].BlindedNodePub,
3✔
416
                )
3✔
417

3✔
418
                // Create a hint which has no fee or cltv delta. We
3✔
419
                // specifically want zero values here because our relay
3✔
420
                // parameters are expressed in encrypted blobs rather than the
3✔
421
                // route itself for blinded routes.
3✔
422
                nextHopIdx := i + 1
3✔
423
                nextNode := route.NewVertex(
3✔
424
                        b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
3✔
425
                )
3✔
426

3✔
427
                edgePolicy := &models.CachedEdgePolicy{
3✔
428
                        ToNodePubKey: func() route.Vertex {
6✔
429
                                return nextNode
3✔
430
                        },
3✔
431
                        ToNodeFeatures: features,
432
                }
433

434
                lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
3✔
435
                if err != nil {
3✔
436
                        return nil, err
×
437
                }
×
438

439
                hints[fromNode] = []AdditionalEdge{lastEdge}
3✔
440
        }
441

442
        return hints, nil
3✔
443
}
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