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

lightningnetwork / lnd / 12165272839

04 Dec 2024 05:35PM UTC coverage: 58.978% (+0.2%) from 58.789%
12165272839

Pull #9316

github

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

125 of 130 new or added lines in 4 files covered. (96.15%)

55 existing lines in 9 files now uncovered.

133554 of 226448 relevant lines covered (58.98%)

19570.84 hits per line

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

88.73
/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) {
13✔
89

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

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

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

106
                if noFeatures {
8✔
107
                        continue
4✔
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
13✔
122

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

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

8✔
143
                        break
8✔
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].
9✔
154
                        CipherText
9✔
155

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

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

170
        return &BlindedPaymentPathSet{
13✔
171
                paths:        pathSet,
13✔
172
                targetPubKey: targetPub,
13✔
173
                features:     features,
13✔
174
                finalCLTV:    finalCLTVDelta,
13✔
175
        }, nil
13✔
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 {
12✔
181
        return s.targetPubKey
12✔
182
}
12✔
183

184
// Features returns the set of relay features available for the payment.
185
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
4✔
186
        return s.features
4✔
187
}
4✔
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) {
4✔
193
        if len(s.paths) != 1 {
4✔
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 {
4✔
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
4✔
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 {
10✔
209
        for _, path := range s.paths {
20✔
210
                introVertex := route.NewVertex(
10✔
211
                        path.BlindedPath.IntroductionPoint,
10✔
212
                )
10✔
213
                if source == introVertex {
11✔
214
                        return true
1✔
215
                }
1✔
216
        }
217

218
        return false
9✔
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 {
8✔
227
        return s.finalCLTV
8✔
228
}
8✔
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 {
8✔
234
        var (
8✔
235
                largestPath *BlindedPayment
8✔
236
                currentMax  int
8✔
237
        )
8✔
238
        for _, path := range s.paths {
16✔
239
                numHops := len(path.BlindedPath.BlindedHops)
8✔
240
                lastHop := path.BlindedPath.BlindedHops[numHops-1]
8✔
241

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

247
        return largestPath
8✔
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) {
7✔
254
        hints := make(RouteHints)
7✔
255

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

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

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

271
        return hints, nil
5✔
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 {
636✔
277
        return bytes.Equal(pk, BlindedPathNUMSKey.SerializeCompressed())
636✔
278
}
636✔
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 {
9✔
316
        if b.BlindedPath == nil {
10✔
317
                return ErrNoBlindedPath
1✔
318
        }
1✔
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 {
9✔
323
                return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
1✔
324
                        len(b.BlindedPath.BlindedHops))
1✔
325
        }
1✔
326

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

332
        return nil
6✔
333
}
334

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

353
        hintCount := len(b.BlindedPath.BlindedHops) - 1
9✔
354
        hints := make(
9✔
355
                RouteHints, hintCount,
9✔
356
        )
9✔
357

9✔
358
        // Start at the unblinded introduction node, because our pathfinding
9✔
359
        // will be able to locate this point in the graph.
9✔
360
        fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
9✔
361

9✔
362
        features := lnwire.EmptyFeatureVector()
9✔
363
        if b.Features != nil {
15✔
364
                features = b.Features.Clone()
6✔
365
        }
6✔
366

367
        // Use the total aggregate relay parameters for the entire blinded
368
        // route as the policy for the hint from our introduction node. This
369
        // will ensure that pathfinding provides sufficient fees/delay for the
370
        // blinded portion to the introduction node.
371
        firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
9✔
372
        edgePolicy := &models.CachedEdgePolicy{
9✔
373
                TimeLockDelta: b.CltvExpiryDelta,
9✔
374
                MinHTLC:       lnwire.MilliSatoshi(b.HtlcMinimum),
9✔
375
                MaxHTLC:       lnwire.MilliSatoshi(b.HtlcMaximum),
9✔
376
                FeeBaseMSat:   lnwire.MilliSatoshi(b.BaseFee),
9✔
377
                FeeProportionalMillionths: lnwire.MilliSatoshi(
9✔
378
                        b.ProportionalFeeRate,
9✔
379
                ),
9✔
380
                ToNodePubKey: func() route.Vertex {
23✔
381
                        return route.NewVertex(
14✔
382
                                // The first node in this slice is
14✔
383
                                // the introduction node, so we start
14✔
384
                                // at index 1 to get the first blinded
14✔
385
                                // relaying node.
14✔
386
                                firstBlindedHop,
14✔
387
                        )
14✔
388
                },
14✔
389
                ToNodeFeatures: features,
390
        }
391

392
        lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
9✔
393
        if err != nil {
9✔
394
                return nil, err
×
395
        }
×
396

397
        hints[fromNode] = []AdditionalEdge{lastEdge}
9✔
398

9✔
399
        // Start at an offset of 1 because the first node in our blinded hops
9✔
400
        // is the introduction node and terminate at the second-last node
9✔
401
        // because we're dealing with hops as pairs.
9✔
402
        for i := 1; i < hintCount; i++ {
26✔
403
                // Set our origin node to the current
17✔
404
                fromNode = route.NewVertex(
17✔
405
                        b.BlindedPath.BlindedHops[i].BlindedNodePub,
17✔
406
                )
17✔
407

17✔
408
                // Create a hint which has no fee or cltv delta. We
17✔
409
                // specifically want zero values here because our relay
17✔
410
                // parameters are expressed in encrypted blobs rather than the
17✔
411
                // route itself for blinded routes.
17✔
412
                nextHopIdx := i + 1
17✔
413
                nextNode := route.NewVertex(
17✔
414
                        b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
17✔
415
                )
17✔
416

17✔
417
                edgePolicy := &models.CachedEdgePolicy{
17✔
418
                        ToNodePubKey: func() route.Vertex {
38✔
419
                                return nextNode
21✔
420
                        },
21✔
421
                        ToNodeFeatures: features,
422
                }
423

424
                lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
17✔
425
                if err != nil {
17✔
426
                        return nil, err
×
427
                }
×
428

429
                hints[fromNode] = []AdditionalEdge{lastEdge}
17✔
430
        }
431

432
        return hints, nil
9✔
433
}
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