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

lightningnetwork / lnd / 13536249039

26 Feb 2025 03:42AM UTC coverage: 57.462% (-1.4%) from 58.835%
13536249039

Pull #8453

github

Roasbeef
peer: update chooseDeliveryScript to gen script if needed

In this commit, we update `chooseDeliveryScript` to generate a new
script if needed. This allows us to fold in a few other lines that
always followed this function into this expanded function.

The tests have been updated accordingly.
Pull Request #8453: [4/4] - multi: integrate new rbf coop close FSM into the existing peer flow

275 of 1318 new or added lines in 22 files covered. (20.86%)

19521 existing lines in 257 files now uncovered.

103858 of 180741 relevant lines covered (57.46%)

24750.23 hits per line

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

86.85
/routing/blinding.go
1
package routing
2

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

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

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

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

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

35
        // ErrHTLCRestrictions is returned when a blinded path has invalid
36
        // HTLC maximum and minimum values.
37
        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()
46
)
47

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

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

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

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

90
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
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.
94
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
95
        error) {
9✔
96

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

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

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

113
                if noFeatures {
2✔
114
                        continue
1✔
115
                }
116

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

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

126
        // Deep copy the paths to avoid mutating the original paths.
127
        pathSet := make([]*BlindedPayment, len(paths))
9✔
128
        for i, path := range paths {
19✔
129
                pathSet[i] = path.deepCopy()
10✔
130
        }
10✔
131

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
9✔
135

9✔
136
        var finalCLTVDelta uint16
9✔
137

9✔
138
        // In case the paths do NOT include a single hop route we append a
9✔
139
        // dummy hop via a NUMS key to allow for MPP path finding via multiple
9✔
140
        // blinded paths. A unified target is needed to use all blinded paths
9✔
141
        // during the payment lifecycle. A dummy hop is solely added for the
9✔
142
        // path finding process and is removed after the path is found. This
9✔
143
        // ensures that we still populate the mission control with the correct
9✔
144
        // data and also respect these mc entries when looking for a path.
9✔
145
        for _, path := range pathSet {
19✔
146
                pathLength := len(path.BlindedPath.BlindedHops)
10✔
147

10✔
148
                // If any provided blinded path only has a single hop (ie, the
10✔
149
                // destination node is also the introduction node), then we
10✔
150
                // discard all other paths since we know the real pub key of the
10✔
151
                // destination node. We also then set the final CLTV delta to
10✔
152
                // the path's delta since there are no other edge hints that
10✔
153
                // will account for it.
10✔
154
                if pathLength == 1 {
14✔
155
                        pathSet = []*BlindedPayment{path}
4✔
156
                        finalCLTVDelta = path.CltvExpiryDelta
4✔
157
                        targetPub = path.BlindedPath.IntroductionPoint
4✔
158

4✔
159
                        break
4✔
160
                }
161

162
                lastHop := path.BlindedPath.BlindedHops[pathLength-1]
6✔
163
                path.BlindedPath.BlindedHops = append(
6✔
164
                        path.BlindedPath.BlindedHops,
6✔
165
                        &sphinx.BlindedHopInfo{
6✔
166
                                BlindedNodePub: &BlindedPathNUMSKey,
6✔
167
                                // We add the last hop's cipher text so that
6✔
168
                                // the payload size of the final hop is equal
6✔
169
                                // to the real last hop.
6✔
170
                                CipherText: lastHop.CipherText,
6✔
171
                        },
6✔
172
                )
6✔
173
        }
174

175
        return &BlindedPaymentPathSet{
9✔
176
                paths:        pathSet,
9✔
177
                targetPubKey: targetPub,
9✔
178
                features:     features,
9✔
179
                finalCLTV:    finalCLTVDelta,
9✔
180
        }, nil
9✔
181
}
182

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

189
// Features returns the set of relay features available for the payment.
UNCOV
190
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
×
UNCOV
191
        return s.features
×
UNCOV
192
}
×
193

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

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

UNCOV
208
        return s.paths[0], nil
×
209
}
210

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

223
        return false
5✔
224
}
225

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

235
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
236
// largest last-hop payload. This is to be used for onion size estimation in
237
// path finding.
238
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() (*BlindedPayment,
239
        error) {
4✔
240

4✔
241
        var (
4✔
242
                largestPath *BlindedPayment
4✔
243
                currentMax  int
4✔
244
        )
4✔
245

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

250
        // We set the largest path to make sure we always return a path even
251
        // if the cipher text is empty.
252
        largestPath = s.paths[0]
4✔
253

4✔
254
        for _, path := range s.paths {
10✔
255
                numHops := len(path.BlindedPath.BlindedHops)
6✔
256
                lastHop := path.BlindedPath.BlindedHops[numHops-1]
6✔
257

6✔
258
                if len(lastHop.CipherText) > currentMax {
10✔
259
                        largestPath = path
4✔
260
                        currentMax = len(lastHop.CipherText)
4✔
261
                }
4✔
262
        }
263

264
        return largestPath, nil
4✔
265
}
266

267
// ToRouteHints converts the blinded path payment set into a RouteHints map so
268
// that the blinded payment paths can be treated like route hints throughout the
269
// code base.
270
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
3✔
271
        hints := make(RouteHints)
3✔
272

3✔
273
        for _, path := range s.paths {
6✔
274
                pathHints, err := path.toRouteHints()
3✔
275
                if err != nil {
3✔
276
                        return nil, err
×
277
                }
×
278

279
                for from, edges := range pathHints {
5✔
280
                        hints[from] = append(hints[from], edges...)
2✔
281
                }
2✔
282
        }
283

284
        if len(hints) == 0 {
5✔
285
                return nil, nil
2✔
286
        }
2✔
287

288
        return hints, nil
1✔
289
}
290

291
// IsBlindedRouteNUMSTargetKey returns true if the given public key is the
292
// NUMS key used as a target for blinded path final hops.
293
func IsBlindedRouteNUMSTargetKey(pk []byte) bool {
3✔
294
        return bytes.Equal(pk, CompressedBlindedPathNUMSKey)
3✔
295
}
3✔
296

297
// BlindedPayment provides the path and payment parameters required to send a
298
// payment along a blinded path.
299
type BlindedPayment struct {
300
        // BlindedPath contains the unblinded introduction point and blinded
301
        // hops for the blinded section of the payment.
302
        BlindedPath *sphinx.BlindedPath
303

304
        // BaseFee is the total base fee to be paid for payments made over the
305
        // blinded path.
306
        BaseFee uint32
307

308
        // ProportionalFeeRate is the aggregated proportional fee rate for
309
        // payments made over the blinded path.
310
        ProportionalFeeRate uint32
311

312
        // CltvExpiryDelta is the total expiry delta for the blinded path. This
313
        // field includes the CLTV for the blinded hops *and* the final cltv
314
        // delta for the receiver.
315
        CltvExpiryDelta uint16
316

317
        // HtlcMinimum is the highest HLTC minimum supported along the blinded
318
        // path (while some hops may have lower values, we're effectively
319
        // bounded by the highest minimum).
320
        HtlcMinimum uint64
321

322
        // HtlcMaximum is the lowest HTLC maximum supported along the blinded
323
        // path (while some hops may have higher values, we're effectively
324
        // bounded by the lowest maximum).
325
        HtlcMaximum uint64
326

327
        // Features is the set of relay features available for the payment.
328
        Features *lnwire.FeatureVector
329
}
330

331
// Validate performs validation on a blinded payment.
332
func (b *BlindedPayment) Validate() error {
5✔
333
        if b.BlindedPath == nil {
6✔
334
                return ErrNoBlindedPath
1✔
335
        }
1✔
336

337
        // The sphinx library inserts the introduction node as the first hop,
338
        // so we expect at least one hop.
339
        if len(b.BlindedPath.BlindedHops) < 1 {
5✔
340
                return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
1✔
341
                        len(b.BlindedPath.BlindedHops))
1✔
342
        }
1✔
343

344
        if b.HtlcMaximum < b.HtlcMinimum {
4✔
345
                return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions,
1✔
346
                        b.HtlcMaximum, b.HtlcMinimum)
1✔
347
        }
1✔
348

349
        for _, hop := range b.BlindedPath.BlindedHops {
6✔
350
                // The first hop of the blinded path does not necessarily have
4✔
351
                // blinded node pub key because it is the introduction point.
4✔
352
                if hop.BlindedNodePub == nil {
6✔
353
                        continue
2✔
354
                }
355

356
                if IsBlindedRouteNUMSTargetKey(
2✔
357
                        hop.BlindedNodePub.SerializeCompressed(),
2✔
358
                ) {
2✔
359

×
360
                        return fmt.Errorf("blinded path cannot include NUMS "+
×
361
                                "key: %s", BlindedPathNUMSHex)
×
362
                }
×
363
        }
364

365
        return nil
2✔
366
}
367

368
// toRouteHints produces a set of chained route hints that represent a blinded
369
// path. In the case of a single hop blinded route (which is paying directly
370
// to the introduction point), no hints will be returned. In this case callers
371
// *must* account for the blinded route's CLTV delta elsewhere (as this is
372
// effectively the final_cltv_delta for the receiving introduction node). In
373
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
374
// hints (both for intermediate hops and the final_cltv_delta for the receiving
375
// node).
376
func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
8✔
377
        // If we just have a single hop in our blinded route, it just contains
8✔
378
        // an introduction node (this is a valid path according to the spec).
8✔
379
        // Since we have the un-blinded node ID for the introduction node, we
8✔
380
        // don't need to add any route hints.
8✔
381
        if len(b.BlindedPath.BlindedHops) == 1 {
11✔
382
                return nil, nil
3✔
383
        }
3✔
384

385
        hintCount := len(b.BlindedPath.BlindedHops) - 1
5✔
386
        hints := make(
5✔
387
                RouteHints, hintCount,
5✔
388
        )
5✔
389

5✔
390
        // Start at the unblinded introduction node, because our pathfinding
5✔
391
        // will be able to locate this point in the graph.
5✔
392
        fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
5✔
393

5✔
394
        features := lnwire.EmptyFeatureVector()
5✔
395
        if b.Features != nil {
7✔
396
                features = b.Features.Clone()
2✔
397
        }
2✔
398

399
        // Use the total aggregate relay parameters for the entire blinded
400
        // route as the policy for the hint from our introduction node. This
401
        // will ensure that pathfinding provides sufficient fees/delay for the
402
        // blinded portion to the introduction node.
403
        firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
5✔
404
        edgePolicy := &models.CachedEdgePolicy{
5✔
405
                TimeLockDelta: b.CltvExpiryDelta,
5✔
406
                MinHTLC:       lnwire.MilliSatoshi(b.HtlcMinimum),
5✔
407
                MaxHTLC:       lnwire.MilliSatoshi(b.HtlcMaximum),
5✔
408
                FeeBaseMSat:   lnwire.MilliSatoshi(b.BaseFee),
5✔
409
                FeeProportionalMillionths: lnwire.MilliSatoshi(
5✔
410
                        b.ProportionalFeeRate,
5✔
411
                ),
5✔
412
                ToNodePubKey: func() route.Vertex {
13✔
413
                        return route.NewVertex(
8✔
414
                                // The first node in this slice is
8✔
415
                                // the introduction node, so we start
8✔
416
                                // at index 1 to get the first blinded
8✔
417
                                // relaying node.
8✔
418
                                firstBlindedHop,
8✔
419
                        )
8✔
420
                },
8✔
421
                ToNodeFeatures: features,
422
        }
423

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

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

5✔
431
        // Start at an offset of 1 because the first node in our blinded hops
5✔
432
        // is the introduction node and terminate at the second-last node
5✔
433
        // because we're dealing with hops as pairs.
5✔
434
        for i := 1; i < hintCount; i++ {
16✔
435
                // Set our origin node to the current
11✔
436
                fromNode = route.NewVertex(
11✔
437
                        b.BlindedPath.BlindedHops[i].BlindedNodePub,
11✔
438
                )
11✔
439

11✔
440
                // Create a hint which has no fee or cltv delta. We
11✔
441
                // specifically want zero values here because our relay
11✔
442
                // parameters are expressed in encrypted blobs rather than the
11✔
443
                // route itself for blinded routes.
11✔
444
                nextHopIdx := i + 1
11✔
445
                nextNode := route.NewVertex(
11✔
446
                        b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
11✔
447
                )
11✔
448

11✔
449
                edgePolicy := &models.CachedEdgePolicy{
11✔
450
                        ToNodePubKey: func() route.Vertex {
24✔
451
                                return nextNode
13✔
452
                        },
13✔
453
                        ToNodeFeatures: features,
454
                }
455

456
                lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
11✔
457
                if err != nil {
11✔
458
                        return nil, err
×
459
                }
×
460

461
                hints[fromNode] = []AdditionalEdge{lastEdge}
11✔
462
        }
463

464
        return hints, nil
5✔
465
}
466

467
// deepCopy returns a deep copy of the BlindedPayment.
468
func (b *BlindedPayment) deepCopy() *BlindedPayment {
12✔
469
        if b == nil {
13✔
470
                return nil
1✔
471
        }
1✔
472

473
        cpyPayment := &BlindedPayment{
11✔
474
                BaseFee:             b.BaseFee,
11✔
475
                ProportionalFeeRate: b.ProportionalFeeRate,
11✔
476
                CltvExpiryDelta:     b.CltvExpiryDelta,
11✔
477
                HtlcMinimum:         b.HtlcMinimum,
11✔
478
                HtlcMaximum:         b.HtlcMaximum,
11✔
479
        }
11✔
480

11✔
481
        // Deep copy the BlindedPath if it exists
11✔
482
        if b.BlindedPath != nil {
22✔
483
                cpyPayment.BlindedPath = &sphinx.BlindedPath{
11✔
484
                        BlindedHops: make([]*sphinx.BlindedHopInfo,
11✔
485
                                len(b.BlindedPath.BlindedHops)),
11✔
486
                }
11✔
487

11✔
488
                if b.BlindedPath.IntroductionPoint != nil {
19✔
489
                        cpyPayment.BlindedPath.IntroductionPoint =
8✔
490
                                copyPublicKey(b.BlindedPath.IntroductionPoint)
8✔
491
                }
8✔
492

493
                if b.BlindedPath.BlindingPoint != nil {
16✔
494
                        cpyPayment.BlindedPath.BlindingPoint =
5✔
495
                                copyPublicKey(b.BlindedPath.BlindingPoint)
5✔
496
                }
5✔
497

498
                // Copy each blinded hop info.
499
                for i, hop := range b.BlindedPath.BlindedHops {
29✔
500
                        if hop == nil {
18✔
501
                                continue
×
502
                        }
503

504
                        cpyHop := &sphinx.BlindedHopInfo{
18✔
505
                                CipherText: hop.CipherText,
18✔
506
                        }
18✔
507

18✔
508
                        if hop.BlindedNodePub != nil {
24✔
509
                                cpyHop.BlindedNodePub =
6✔
510
                                        copyPublicKey(hop.BlindedNodePub)
6✔
511
                        }
6✔
512

513
                        cpyHop.CipherText = make([]byte, len(hop.CipherText))
18✔
514
                        copy(cpyHop.CipherText, hop.CipherText)
18✔
515

18✔
516
                        cpyPayment.BlindedPath.BlindedHops[i] = cpyHop
18✔
517
                }
518
        }
519

520
        // Deep copy the Features if they exist
521
        if b.Features != nil {
13✔
522
                cpyPayment.Features = b.Features.Clone()
2✔
523
        }
2✔
524

525
        return cpyPayment
11✔
526
}
527

528
// copyPublicKey makes a deep copy of a public key.
529
//
530
// TODO(ziggie): Remove this function if this is available in the btcec library.
531
func copyPublicKey(pk *btcec.PublicKey) *btcec.PublicKey {
19✔
532
        var result secp256k1.JacobianPoint
19✔
533
        pk.AsJacobian(&result)
19✔
534
        result.ToAffine()
19✔
535

19✔
536
        return btcec.NewPublicKey(&result.X, &result.Y)
19✔
537
}
19✔
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