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

lightningnetwork / lnd / 12155633294

04 Dec 2024 08:15AM UTC coverage: 58.959% (+0.003%) from 58.956%
12155633294

Pull #9316

github

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

119 of 125 new or added lines in 4 files covered. (95.2%)

71 existing lines in 15 files now uncovered.

133506 of 226438 relevant lines covered (58.96%)

19524.28 hits per line

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

88.46
/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).
18
const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
19
        "dd6e95f5e299dfd54e"
20

21
var (
22
        // ErrNoBlindedPath is returned when the blinded path in a blinded
23
        // payment is missing.
24
        ErrNoBlindedPath = errors.New("blinded path required")
25

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

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

35
        // BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
36
        // has no known private key. This was generated using the following
37
        // script:
38
        // https://github.com/lightninglabs/lightning-node-connect/tree/master/
39
        // mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
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
                path.BlindedPath.BlindedHops = append(
9✔
157
                        path.BlindedPath.BlindedHops,
9✔
158
                        &sphinx.BlindedHopInfo{
9✔
159
                                BlindedNodePub: &BlindedPathNUMSKey,
9✔
160
                                CipherText:     lastHopCipherText,
9✔
161
                        },
9✔
162
                )
9✔
163
        }
164

165
        return &BlindedPaymentPathSet{
13✔
166
                paths:        pathSet,
13✔
167
                targetPubKey: targetPub,
13✔
168
                features:     features,
13✔
169
                finalCLTV:    finalCLTVDelta,
13✔
170
        }, nil
13✔
171
}
172

173
// TargetPubKey returns the public key to be used as the destination node's
174
// public key during pathfinding.
175
func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
12✔
176
        return s.targetPubKey
12✔
177
}
12✔
178

179
// Features returns the set of relay features available for the payment.
180
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
4✔
181
        return s.features
4✔
182
}
4✔
183

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

193
        if len(s.paths[0].BlindedPath.BlindedHops) > 1 {
4✔
194
                return nil, fmt.Errorf("an intro node only path cannot have " +
×
195
                        "more than one hop")
×
196
        }
×
197

198
        return s.paths[0], nil
4✔
199
}
200

201
// IsIntroNode returns true if the given vertex is an introduction node for one
202
// of the paths in the blinded payment path set.
203
func (s *BlindedPaymentPathSet) IsIntroNode(source route.Vertex) bool {
10✔
204
        for _, path := range s.paths {
20✔
205
                introVertex := route.NewVertex(
10✔
206
                        path.BlindedPath.IntroductionPoint,
10✔
207
                )
10✔
208
                if source == introVertex {
11✔
209
                        return true
1✔
210
                }
1✔
211
        }
212

213
        return false
9✔
214
}
215

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

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

8✔
237
                if len(lastHop.CipherText) > currentMax {
16✔
238
                        largestPath = path
8✔
239
                }
8✔
240
        }
241

242
        return largestPath
8✔
243
}
244

245
// ToRouteHints converts the blinded path payment set into a RouteHints map so
246
// that the blinded payment paths can be treated like route hints throughout the
247
// code base.
248
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
7✔
249
        hints := make(RouteHints)
7✔
250

7✔
251
        for _, path := range s.paths {
14✔
252
                pathHints, err := path.toRouteHints()
7✔
253
                if err != nil {
7✔
254
                        return nil, err
×
255
                }
×
256

257
                for from, edges := range pathHints {
14✔
258
                        hints[from] = append(hints[from], edges...)
7✔
259
                }
7✔
260
        }
261

262
        if len(hints) == 0 {
13✔
263
                return nil, nil
6✔
264
        }
6✔
265

266
        return hints, nil
5✔
267
}
268

269
// isBlindedRouteNUMSTargetKey returns true if the given public key is the
270
// NUMS key used as a target for blinded path final hops.
271
func isBlindedRouteNUMSTargetKey(pk []byte) bool {
5✔
272
        return bytes.Equal(pk, BlindedPathNUMSKey.SerializeCompressed())
5✔
273
}
5✔
274

275
// BlindedPayment provides the path and payment parameters required to send a
276
// payment along a blinded path.
277
type BlindedPayment struct {
278
        // BlindedPath contains the unblinded introduction point and blinded
279
        // hops for the blinded section of the payment.
280
        BlindedPath *sphinx.BlindedPath
281

282
        // BaseFee is the total base fee to be paid for payments made over the
283
        // blinded path.
284
        BaseFee uint32
285

286
        // ProportionalFeeRate is the aggregated proportional fee rate for
287
        // payments made over the blinded path.
288
        ProportionalFeeRate uint32
289

290
        // CltvExpiryDelta is the total expiry delta for the blinded path. This
291
        // field includes the CLTV for the blinded hops *and* the final cltv
292
        // delta for the receiver.
293
        CltvExpiryDelta uint16
294

295
        // HtlcMinimum is the highest HLTC minimum supported along the blinded
296
        // path (while some hops may have lower values, we're effectively
297
        // bounded by the highest minimum).
298
        HtlcMinimum uint64
299

300
        // HtlcMaximum is the lowest HTLC maximum supported along the blinded
301
        // path (while some hops may have higher values, we're effectively
302
        // bounded by the lowest maximum).
303
        HtlcMaximum uint64
304

305
        // Features is the set of relay features available for the payment.
306
        Features *lnwire.FeatureVector
307
}
308

309
// Validate performs validation on a blinded payment.
310
func (b *BlindedPayment) Validate() error {
9✔
311
        if b.BlindedPath == nil {
10✔
312
                return ErrNoBlindedPath
1✔
313
        }
1✔
314

315
        // The sphinx library inserts the introduction node as the first hop,
316
        // so we expect at least one hop.
317
        if len(b.BlindedPath.BlindedHops) < 1 {
9✔
318
                return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
1✔
319
                        len(b.BlindedPath.BlindedHops))
1✔
320
        }
1✔
321

322
        if b.HtlcMaximum < b.HtlcMinimum {
8✔
323
                return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions,
1✔
324
                        b.HtlcMaximum, b.HtlcMinimum)
1✔
325
        }
1✔
326

327
        return nil
6✔
328
}
329

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

348
        hintCount := len(b.BlindedPath.BlindedHops) - 1
9✔
349
        hints := make(
9✔
350
                RouteHints, hintCount,
9✔
351
        )
9✔
352

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

9✔
357
        features := lnwire.EmptyFeatureVector()
9✔
358
        if b.Features != nil {
15✔
359
                features = b.Features.Clone()
6✔
360
        }
6✔
361

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

387
        lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
9✔
388
        if err != nil {
9✔
389
                return nil, err
×
390
        }
×
391

392
        hints[fromNode] = []AdditionalEdge{lastEdge}
9✔
393

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

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

16✔
412
                edgePolicy := &models.CachedEdgePolicy{
16✔
413
                        ToNodePubKey: func() route.Vertex {
27✔
414
                                return nextNode
11✔
415
                        },
11✔
416
                        ToNodeFeatures: features,
417
                }
418

419
                lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
16✔
420
                if err != nil {
16✔
421
                        return nil, err
×
422
                }
×
423

424
                hints[fromNode] = []AdditionalEdge{lastEdge}
16✔
425
        }
426

427
        return hints, nil
9✔
428
}
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