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

lightningnetwork / lnd / 11292787765

11 Oct 2024 12:58PM UTC coverage: 49.179% (-9.5%) from 58.716%
11292787765

push

github

web-flow
Merge pull request #9168 from feelancer21/fix-lncli-wallet-proto

lnrpc: fix lncli documentation tags in walletkit.proto

97369 of 197987 relevant lines covered (49.18%)

1.04 hits per line

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

81.55
/routing/blinding.go
1
package routing
2

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

7
        "github.com/btcsuite/btcd/btcec/v2"
8
        sphinx "github.com/lightningnetwork/lightning-onion"
9
        "github.com/lightningnetwork/lnd/channeldb/models"
10
        "github.com/lightningnetwork/lnd/fn"
11
        "github.com/lightningnetwork/lnd/lnwire"
12
        "github.com/lightningnetwork/lnd/routing/route"
13
)
14

15
var (
16
        // ErrNoBlindedPath is returned when the blinded path in a blinded
17
        // payment is missing.
18
        ErrNoBlindedPath = errors.New("blinded path required")
19

20
        // ErrInsufficientBlindedHops is returned when a blinded path does
21
        // not have enough blinded hops.
22
        ErrInsufficientBlindedHops = errors.New("blinded path requires " +
23
                "at least one hop")
24

25
        // ErrHTLCRestrictions is returned when a blinded path has invalid
26
        // HTLC maximum and minimum values.
27
        ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
28
)
29

30
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
31
// blinded paths provided by the recipient of a payment.
32
//
33
// NOTE: for now this only holds a single BlindedPayment. By the end of the PR
34
// series, it will handle multiple paths.
35
type BlindedPaymentPathSet struct {
36
        // paths is the set of blinded payment paths for a single payment.
37
        // NOTE: For now this will always only have a single entry. By the end
38
        // of this PR, it can hold multiple.
39
        paths []*BlindedPayment
40

41
        // targetPubKey is the ephemeral node pub key that we will inject into
42
        // each path as the last hop. This is only for the sake of path finding.
43
        // Once the path has been found, the original destination pub key is
44
        // used again. In the edge case where there is only a single hop in the
45
        // path (the introduction node is the destination node), then this will
46
        // just be the introduction node's real public key.
47
        targetPubKey *btcec.PublicKey
48

49
        // features is the set of relay features available for the payment.
50
        // This is extracted from the set of blinded payment paths. At the
51
        // moment we require that all paths for the same payment have the
52
        // same feature set.
53
        features *lnwire.FeatureVector
54

55
        // finalCLTV is the final hop's expiry delta of _any_ path in the set.
56
        // For any multi-hop path, the final CLTV delta should be seen as zero
57
        // since the final hop's final CLTV delta is accounted for in the
58
        // accumulated path policy values. The only edge case is for when the
59
        // final hop in the path is also the introduction node in which case
60
        // that path's FinalCLTV must be the non-zero min CLTV of the final hop
61
        // so that it is accounted for in path finding. For this reason, if
62
        // we have any single path in the set with only one hop, then we throw
63
        // away all the other paths. This should be fine to do since if there is
64
        // a path where the intro node is also the destination node, then there
65
        // isn't any need to try any other longer blinded path. In other words,
66
        // if this value is non-zero, then there is only one path in this
67
        // blinded path set and that path only has a single hop: the
68
        // introduction node.
69
        finalCLTV uint16
70
}
71

72
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
73
// BlindedPayments.
74
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
75
        error) {
2✔
76

2✔
77
        if len(paths) == 0 {
2✔
78
                return nil, ErrNoBlindedPath
×
79
        }
×
80

81
        // For now, we assert that all the paths have the same set of features.
82
        features := paths[0].Features
2✔
83
        noFeatures := features == nil || features.IsEmpty()
2✔
84
        for i := 1; i < len(paths); i++ {
4✔
85
                noFeats := paths[i].Features == nil ||
2✔
86
                        paths[i].Features.IsEmpty()
2✔
87

2✔
88
                if noFeatures && !noFeats {
2✔
89
                        return nil, fmt.Errorf("all blinded paths must have " +
×
90
                                "the same set of features")
×
91
                }
×
92

93
                if noFeatures {
4✔
94
                        continue
2✔
95
                }
96

97
                if !features.RawFeatureVector.Equals(
×
98
                        paths[i].Features.RawFeatureVector,
×
99
                ) {
×
100

×
101
                        return nil, fmt.Errorf("all blinded paths must have " +
×
102
                                "the same set of features")
×
103
                }
×
104
        }
105

106
        // Derive an ephemeral target priv key that will be injected into each
107
        // blinded path final hop.
108
        targetPriv, err := btcec.NewPrivateKey()
2✔
109
        if err != nil {
2✔
110
                return nil, err
×
111
        }
×
112
        targetPub := targetPriv.PubKey()
2✔
113

2✔
114
        var (
2✔
115
                pathSet        = paths
2✔
116
                finalCLTVDelta uint16
2✔
117
        )
2✔
118
        // If any provided blinded path only has a single hop (ie, the
2✔
119
        // destination node is also the introduction node), then we discard all
2✔
120
        // other paths since we know the real pub key of the destination node.
2✔
121
        // We also then set the final CLTV delta to the path's delta since
2✔
122
        // there are no other edge hints that will account for it. For a single
2✔
123
        // hop path, there is also no need for the pseudo target pub key
2✔
124
        // replacement, so our target pub key in this case just remains the
2✔
125
        // real introduction node ID.
2✔
126
        for _, path := range paths {
4✔
127
                if len(path.BlindedPath.BlindedHops) != 1 {
4✔
128
                        continue
2✔
129
                }
130

131
                pathSet = []*BlindedPayment{path}
2✔
132
                finalCLTVDelta = path.CltvExpiryDelta
2✔
133
                targetPub = path.BlindedPath.IntroductionPoint
2✔
134

2✔
135
                break
2✔
136
        }
137

138
        return &BlindedPaymentPathSet{
2✔
139
                paths:        pathSet,
2✔
140
                targetPubKey: targetPub,
2✔
141
                features:     features,
2✔
142
                finalCLTV:    finalCLTVDelta,
2✔
143
        }, nil
2✔
144
}
145

146
// TargetPubKey returns the public key to be used as the destination node's
147
// public key during pathfinding.
148
func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
2✔
149
        return s.targetPubKey
2✔
150
}
2✔
151

152
// Features returns the set of relay features available for the payment.
153
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
2✔
154
        return s.features
2✔
155
}
2✔
156

157
// IntroNodeOnlyPath can be called if it is expected that the path set only
158
// contains a single payment path which itself only has one hop. It errors if
159
// this is not the case.
160
func (s *BlindedPaymentPathSet) IntroNodeOnlyPath() (*BlindedPayment, error) {
2✔
161
        if len(s.paths) != 1 {
2✔
162
                return nil, fmt.Errorf("expected only a single path in the "+
×
163
                        "blinded payment set, got %d", len(s.paths))
×
164
        }
×
165

166
        if len(s.paths[0].BlindedPath.BlindedHops) > 1 {
2✔
167
                return nil, fmt.Errorf("an intro node only path cannot have " +
×
168
                        "more than one hop")
×
169
        }
×
170

171
        return s.paths[0], nil
2✔
172
}
173

174
// IsIntroNode returns true if the given vertex is an introduction node for one
175
// of the paths in the blinded payment path set.
176
func (s *BlindedPaymentPathSet) IsIntroNode(source route.Vertex) bool {
2✔
177
        for _, path := range s.paths {
4✔
178
                introVertex := route.NewVertex(
2✔
179
                        path.BlindedPath.IntroductionPoint,
2✔
180
                )
2✔
181
                if source == introVertex {
2✔
182
                        return true
×
183
                }
×
184
        }
185

186
        return false
2✔
187
}
188

189
// FinalCLTVDelta is the minimum CLTV delta to use for the final hop on the
190
// route. In most cases this will return zero since the value is accounted for
191
// in the path's accumulated CLTVExpiryDelta. Only in the edge case of the path
192
// set only including a single path which only includes an introduction node
193
// will this return a non-zero value.
194
func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
2✔
195
        return s.finalCLTV
2✔
196
}
2✔
197

198
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
199
// largest last-hop payload. This is to be used for onion size estimation in
200
// path finding.
201
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
2✔
202
        var (
2✔
203
                largestPath *BlindedPayment
2✔
204
                currentMax  int
2✔
205
        )
2✔
206
        for _, path := range s.paths {
4✔
207
                numHops := len(path.BlindedPath.BlindedHops)
2✔
208
                lastHop := path.BlindedPath.BlindedHops[numHops-1]
2✔
209

2✔
210
                if len(lastHop.CipherText) > currentMax {
4✔
211
                        largestPath = path
2✔
212
                }
2✔
213
        }
214

215
        return largestPath
2✔
216
}
217

218
// ToRouteHints converts the blinded path payment set into a RouteHints map so
219
// that the blinded payment paths can be treated like route hints throughout the
220
// code base.
221
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
2✔
222
        hints := make(RouteHints)
2✔
223

2✔
224
        for _, path := range s.paths {
4✔
225
                pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
2✔
226
                if err != nil {
2✔
227
                        return nil, err
×
228
                }
×
229

230
                for from, edges := range pathHints {
4✔
231
                        hints[from] = append(hints[from], edges...)
2✔
232
                }
2✔
233
        }
234

235
        if len(hints) == 0 {
4✔
236
                return nil, nil
2✔
237
        }
2✔
238

239
        return hints, nil
2✔
240
}
241

242
// BlindedPayment provides the path and payment parameters required to send a
243
// payment along a blinded path.
244
type BlindedPayment struct {
245
        // BlindedPath contains the unblinded introduction point and blinded
246
        // hops for the blinded section of the payment.
247
        BlindedPath *sphinx.BlindedPath
248

249
        // BaseFee is the total base fee to be paid for payments made over the
250
        // blinded path.
251
        BaseFee uint32
252

253
        // ProportionalFeeRate is the aggregated proportional fee rate for
254
        // payments made over the blinded path.
255
        ProportionalFeeRate uint32
256

257
        // CltvExpiryDelta is the total expiry delta for the blinded path. This
258
        // field includes the CLTV for the blinded hops *and* the final cltv
259
        // delta for the receiver.
260
        CltvExpiryDelta uint16
261

262
        // HtlcMinimum is the highest HLTC minimum supported along the blinded
263
        // path (while some hops may have lower values, we're effectively
264
        // bounded by the highest minimum).
265
        HtlcMinimum uint64
266

267
        // HtlcMaximum is the lowest HTLC maximum supported along the blinded
268
        // path (while some hops may have higher values, we're effectively
269
        // bounded by the lowest maximum).
270
        HtlcMaximum uint64
271

272
        // Features is the set of relay features available for the payment.
273
        Features *lnwire.FeatureVector
274
}
275

276
// Validate performs validation on a blinded payment.
277
func (b *BlindedPayment) Validate() error {
2✔
278
        if b.BlindedPath == nil {
2✔
279
                return ErrNoBlindedPath
×
280
        }
×
281

282
        // The sphinx library inserts the introduction node as the first hop,
283
        // so we expect at least one hop.
284
        if len(b.BlindedPath.BlindedHops) < 1 {
2✔
285
                return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
×
286
                        len(b.BlindedPath.BlindedHops))
×
287
        }
×
288

289
        if b.HtlcMaximum < b.HtlcMinimum {
2✔
290
                return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions,
×
291
                        b.HtlcMaximum, b.HtlcMinimum)
×
292
        }
×
293

294
        return nil
2✔
295
}
296

297
// toRouteHints produces a set of chained route hints that represent a blinded
298
// path. In the case of a single hop blinded route (which is paying directly
299
// to the introduction point), no hints will be returned. In this case callers
300
// *must* account for the blinded route's CLTV delta elsewhere (as this is
301
// effectively the final_cltv_delta for the receiving introduction node). In
302
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
303
// hints (both for intermediate hops and the final_cltv_delta for the receiving
304
// node). The pseudoTarget, if provided,  will be used to override the pub key
305
// of the destination node in the path.
306
func (b *BlindedPayment) toRouteHints(
307
        pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
2✔
308

2✔
309
        // If we just have a single hop in our blinded route, it just contains
2✔
310
        // an introduction node (this is a valid path according to the spec).
2✔
311
        // Since we have the un-blinded node ID for the introduction node, we
2✔
312
        // don't need to add any route hints.
2✔
313
        if len(b.BlindedPath.BlindedHops) == 1 {
4✔
314
                return nil, nil
2✔
315
        }
2✔
316

317
        hintCount := len(b.BlindedPath.BlindedHops) - 1
2✔
318
        hints := make(
2✔
319
                RouteHints, hintCount,
2✔
320
        )
2✔
321

2✔
322
        // Start at the unblinded introduction node, because our pathfinding
2✔
323
        // will be able to locate this point in the graph.
2✔
324
        fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
2✔
325

2✔
326
        features := lnwire.EmptyFeatureVector()
2✔
327
        if b.Features != nil {
4✔
328
                features = b.Features.Clone()
2✔
329
        }
2✔
330

331
        // Use the total aggregate relay parameters for the entire blinded
332
        // route as the policy for the hint from our introduction node. This
333
        // will ensure that pathfinding provides sufficient fees/delay for the
334
        // blinded portion to the introduction node.
335
        firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
2✔
336
        edgePolicy := &models.CachedEdgePolicy{
2✔
337
                TimeLockDelta: b.CltvExpiryDelta,
2✔
338
                MinHTLC:       lnwire.MilliSatoshi(b.HtlcMinimum),
2✔
339
                MaxHTLC:       lnwire.MilliSatoshi(b.HtlcMaximum),
2✔
340
                FeeBaseMSat:   lnwire.MilliSatoshi(b.BaseFee),
2✔
341
                FeeProportionalMillionths: lnwire.MilliSatoshi(
2✔
342
                        b.ProportionalFeeRate,
2✔
343
                ),
2✔
344
                ToNodePubKey: func() route.Vertex {
4✔
345
                        return route.NewVertex(
2✔
346
                                // The first node in this slice is
2✔
347
                                // the introduction node, so we start
2✔
348
                                // at index 1 to get the first blinded
2✔
349
                                // relaying node.
2✔
350
                                firstBlindedHop,
2✔
351
                        )
2✔
352
                },
2✔
353
                ToNodeFeatures: features,
354
        }
355

356
        lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
2✔
357
        if err != nil {
2✔
358
                return nil, err
×
359
        }
×
360

361
        hints[fromNode] = []AdditionalEdge{lastEdge}
2✔
362

2✔
363
        // Start at an offset of 1 because the first node in our blinded hops
2✔
364
        // is the introduction node and terminate at the second-last node
2✔
365
        // because we're dealing with hops as pairs.
2✔
366
        for i := 1; i < hintCount; i++ {
4✔
367
                // Set our origin node to the current
2✔
368
                fromNode = route.NewVertex(
2✔
369
                        b.BlindedPath.BlindedHops[i].BlindedNodePub,
2✔
370
                )
2✔
371

2✔
372
                // Create a hint which has no fee or cltv delta. We
2✔
373
                // specifically want zero values here because our relay
2✔
374
                // parameters are expressed in encrypted blobs rather than the
2✔
375
                // route itself for blinded routes.
2✔
376
                nextHopIdx := i + 1
2✔
377
                nextNode := route.NewVertex(
2✔
378
                        b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
2✔
379
                )
2✔
380

2✔
381
                edgePolicy := &models.CachedEdgePolicy{
2✔
382
                        ToNodePubKey: func() route.Vertex {
2✔
383
                                return nextNode
×
384
                        },
×
385
                        ToNodeFeatures: features,
386
                }
387

388
                lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
2✔
389
                if err != nil {
2✔
390
                        return nil, err
×
391
                }
×
392

393
                hints[fromNode] = []AdditionalEdge{lastEdge}
2✔
394
        }
395

396
        pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
4✔
397
                // For the very last hop on the path, switch out the ToNodePub
2✔
398
                // for the pseudo target pub key.
2✔
399
                lastEdge.policy.ToNodePubKey = func() route.Vertex {
4✔
400
                        return route.NewVertex(key)
2✔
401
                }
2✔
402

403
                // Then override the final hint with this updated edge.
404
                hints[fromNode] = []AdditionalEdge{lastEdge}
2✔
405
        })
406

407
        return hints, nil
2✔
408
}
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