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

lightningnetwork / lnd / 14695690618

27 Apr 2025 08:09PM UTC coverage: 58.581% (-0.008%) from 58.589%
14695690618

Pull #9127

github

web-flow
Merge 6de35e3d5 into 7e50b8438
Pull Request #9127: Add the option on path creator to specify the incoming channel on blinded path

103 of 130 new or added lines in 5 files covered. (79.23%)

183 existing lines in 17 files now uncovered.

97463 of 166374 relevant lines covered (58.58%)

1.82 hits per line

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

84.76
/routing/blindedpath/blinded_path.go
1
package blindedpath
2

3
import (
4
        "bytes"
5
        "errors"
6
        "fmt"
7
        "math"
8
        "sort"
9

10
        "github.com/btcsuite/btcd/btcec/v2"
11
        "github.com/btcsuite/btcd/btcutil"
12
        sphinx "github.com/lightningnetwork/lightning-onion"
13
        "github.com/lightningnetwork/lnd/channeldb"
14
        "github.com/lightningnetwork/lnd/graph/db/models"
15
        "github.com/lightningnetwork/lnd/lnwire"
16
        "github.com/lightningnetwork/lnd/record"
17
        "github.com/lightningnetwork/lnd/routing/route"
18
        "github.com/lightningnetwork/lnd/tlv"
19
        "github.com/lightningnetwork/lnd/zpay32"
20
)
21

22
const (
23
        // oneMillion is a constant used frequently in fee rate calculations.
24
        oneMillion = uint32(1_000_000)
25
)
26

27
// errInvalidBlindedPath indicates that the chosen real path is not usable as
28
// a blinded path.
29
var errInvalidBlindedPath = errors.New("the chosen path results in an " +
30
        "unusable blinded path")
31

32
// BuildBlindedPathCfg defines the various resources and configuration values
33
// required to build a blinded payment path to this node.
34
type BuildBlindedPathCfg struct {
35
        // FindRoutes returns a set of routes to us that can be used for the
36
        // construction of blinded paths. These routes will consist of real
37
        // nodes advertising the route blinding feature bit. They may be of
38
        // various lengths and may even contain only a single hop. Any route
39
        // shorter than MinNumHops will be padded with dummy hops during route
40
        // construction.
41
        FindRoutes func(value lnwire.MilliSatoshi) ([]*route.Route, error)
42

43
        // FetchChannelEdgesByID attempts to look up the two directed edges for
44
        // the channel identified by the channel ID.
45
        FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
46
                *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error)
47

48
        // FetchNodeFeatures returns the features of the given node.
49
        FetchNodeFeatures func(route.Vertex) (*lnwire.FeatureVector, error)
50

51
        // FetchOurOpenChannels fetches this node's set of open channels.
52
        FetchOurOpenChannels func() ([]*channeldb.OpenChannel, error)
53

54
        // BestHeight can be used to fetch the best block height that this node
55
        // is aware of.
56
        BestHeight func() (uint32, error)
57

58
        // AddPolicyBuffer is a function that can be used to alter the policy
59
        // values of the given channel edge. The main reason for doing this is
60
        // to add a safety buffer so that if the node makes small policy changes
61
        // during the lifetime of the blinded path, then the path remains valid
62
        // and so probing is more difficult. Note that this will only be called
63
        // for the policies of real nodes and won't be applied to
64
        // DefaultDummyHopPolicy.
65
        AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
66
                error)
67

68
        // PathID is the secret data to embed in the blinded path data that we
69
        // will receive back as the recipient. This is the equivalent of the
70
        // payment address used in normal payments. It lets the recipient check
71
        // that the path is being used in the correct context.
72
        PathID []byte
73

74
        // ValueMsat is the payment amount in milli-satoshis that must be
75
        // routed. This will be used for selecting appropriate routes to use for
76
        // the blinded path.
77
        ValueMsat lnwire.MilliSatoshi
78

79
        // MinFinalCLTVExpiryDelta is the minimum CLTV delta that the recipient
80
        // requires for the final hop of the payment.
81
        //
82
        // NOTE that the caller is responsible for adding additional block
83
        // padding to this value to account for blocks being mined while the
84
        // payment is in-flight.
85
        MinFinalCLTVExpiryDelta uint32
86

87
        // BlocksUntilExpiry is the number of blocks that this blinded path
88
        // should remain valid for. This is a relative number of blocks. This
89
        // number in addition with a potential minimum cltv delta for the last
90
        // hop and some block padding will be the payment constraint which is
91
        // part of the blinded hop info. Every htlc using the provided blinded
92
        // hops cannot have a higher cltv delta otherwise it will get rejected
93
        // by the forwarding nodes or the final node.
94
        //
95
        // This number should at least be greater than the invoice expiry time
96
        // so that the blinded route is always valid as long as the invoice is
97
        // valid.
98
        BlocksUntilExpiry uint32
99

100
        // MinNumHops is the minimum number of hops that each blinded path
101
        // should be. If the number of hops in a path returned by FindRoutes is
102
        // less than this number, then dummy hops will be post-fixed to the
103
        // route.
104
        MinNumHops uint8
105

106
        // DefaultDummyHopPolicy holds the policy values that should be used for
107
        // dummy hops in the cases where it cannot be derived via other means
108
        // such as averaging the policy values of other hops on the path. This
109
        // would happen in the case where the introduction node is also the
110
        // introduction node. If these default policy values are used, then
111
        // the MaxHTLCMsat value must be carefully chosen.
112
        DefaultDummyHopPolicy *BlindedHopPolicy
113
}
114

115
// BuildBlindedPaymentPaths uses the passed config to construct a set of blinded
116
// payment paths that can be added to the invoice.
117
func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) (
118
        []*zpay32.BlindedPaymentPath, error) {
3✔
119

3✔
120
        // Find some appropriate routes for the value to be routed. This will
3✔
121
        // return a set of routes made up of real nodes.
3✔
122
        routes, err := cfg.FindRoutes(cfg.ValueMsat)
3✔
123
        if err != nil {
6✔
124
                return nil, err
3✔
125
        }
3✔
126

127
        if len(routes) == 0 {
3✔
UNCOV
128
                return nil, fmt.Errorf("could not find any routes to self to " +
×
UNCOV
129
                        "use for blinded route construction")
×
UNCOV
130
        }
×
131

132
        // Not every route returned will necessarily result in a usable blinded
133
        // path and so the number of paths returned might be less than the
134
        // number of real routes returned by FindRoutes above.
135
        paths := make([]*zpay32.BlindedPaymentPath, 0, len(routes))
3✔
136

3✔
137
        // For each route returned, we will construct the associated blinded
3✔
138
        // payment path.
3✔
139
        for _, route := range routes {
6✔
140
                // Extract the information we need from the route.
3✔
141
                candidatePath := extractCandidatePath(route)
3✔
142

3✔
143
                // Pad the given route with dummy hops until the minimum number
3✔
144
                // of hops is met.
3✔
145
                candidatePath.padWithDummyHops(cfg.MinNumHops)
3✔
146

3✔
147
                path, err := buildBlindedPaymentPath(cfg, candidatePath)
3✔
148
                if errors.Is(err, errInvalidBlindedPath) {
3✔
149
                        log.Debugf("Not using route (%s) as a blinded path "+
×
150
                                "since it resulted in an invalid blinded path",
×
UNCOV
151
                                route)
×
152

×
153
                        continue
×
154
                } else if err != nil {
3✔
155
                        log.Errorf("Not using route (%s) as a blinded path: %v",
×
UNCOV
156
                                route, err)
×
UNCOV
157

×
UNCOV
158
                        continue
×
159
                }
160

161
                log.Debugf("Route selected for blinded path: %s", candidatePath)
3✔
162

3✔
163
                paths = append(paths, path)
3✔
164
        }
165

166
        if len(paths) == 0 {
3✔
UNCOV
167
                return nil, fmt.Errorf("could not build any blinded paths")
×
UNCOV
168
        }
×
169

170
        return paths, nil
3✔
171
}
172

173
// buildBlindedPaymentPath takes a route from an introduction node to this node
174
// and uses the given config to convert it into a blinded payment path.
175
func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) (
176
        *zpay32.BlindedPaymentPath, error) {
3✔
177

3✔
178
        hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path)
3✔
179
        if err != nil {
3✔
UNCOV
180
                return nil, fmt.Errorf("could not collect blinded path relay "+
×
UNCOV
181
                        "info: %w", err)
×
UNCOV
182
        }
×
183

184
        relayInfo := make([]*record.PaymentRelayInfo, len(hops))
3✔
185
        for i, hop := range hops {
6✔
186
                relayInfo[i] = hop.relayInfo
3✔
187
        }
3✔
188

189
        // Using the collected relay info, we can calculate the aggregated
190
        // policy values for the route.
191
        baseFee, feeRate, cltvDelta := calcBlindedPathPolicies(
3✔
192
                relayInfo, uint16(cfg.MinFinalCLTVExpiryDelta),
3✔
193
        )
3✔
194

3✔
195
        currentHeight, err := cfg.BestHeight()
3✔
196
        if err != nil {
3✔
UNCOV
197
                return nil, err
×
UNCOV
198
        }
×
199

200
        // The next step is to calculate the payment constraints to communicate
201
        // to each hop and to package up the hop info for each hop. We will
202
        // handle the final hop first since its payload looks a bit different,
203
        // and then we will iterate backwards through the remaining hops.
204
        //
205
        // Note that the +1 here is required because the route won't have the
206
        // introduction node included in the "Hops". But since we want to create
207
        // payloads for all the hops as well as the introduction node, we add 1
208
        // here to get the full hop length along with the introduction node.
209
        hopDataSet := make([]*hopData, 0, len(path.hops)+1)
3✔
210

3✔
211
        // Determine the maximum CLTV expiry for the destination node.
3✔
212
        cltvExpiry := currentHeight + cfg.BlocksUntilExpiry +
3✔
213
                cfg.MinFinalCLTVExpiryDelta
3✔
214

3✔
215
        constraints := &record.PaymentConstraints{
3✔
216
                MaxCltvExpiry:   cltvExpiry,
3✔
217
                HtlcMinimumMsat: minHTLC,
3✔
218
        }
3✔
219

3✔
220
        // If the blinded route has only a source node (introduction node) and
3✔
221
        // no hops, then the destination node is also the source node.
3✔
222
        finalHopPubKey := path.introNode
3✔
223
        if len(path.hops) > 0 {
6✔
224
                finalHopPubKey = path.hops[len(path.hops)-1].pubKey
3✔
225
        }
3✔
226

227
        // For the final hop, we only send it the path ID and payment
228
        // constraints.
229
        info, err := buildFinalHopRouteData(
3✔
230
                finalHopPubKey, cfg.PathID, constraints,
3✔
231
        )
3✔
232
        if err != nil {
3✔
UNCOV
233
                return nil, err
×
UNCOV
234
        }
×
235

236
        hopDataSet = append(hopDataSet, info)
3✔
237

3✔
238
        // Iterate through the remaining (non-final) hops, back to front.
3✔
239
        for i := len(hops) - 1; i >= 0; i-- {
6✔
240
                hop := hops[i]
3✔
241

3✔
242
                cltvExpiry += uint32(hop.relayInfo.CltvExpiryDelta)
3✔
243

3✔
244
                constraints = &record.PaymentConstraints{
3✔
245
                        MaxCltvExpiry:   cltvExpiry,
3✔
246
                        HtlcMinimumMsat: minHTLC,
3✔
247
                }
3✔
248

3✔
249
                var info *hopData
3✔
250
                if hop.nextHopIsDummy {
6✔
251
                        info, err = buildDummyRouteData(
3✔
252
                                hop.hopPubKey, hop.relayInfo, constraints,
3✔
253
                        )
3✔
254
                } else {
6✔
255
                        info, err = buildHopRouteData(
3✔
256
                                hop.hopPubKey, hop.nextSCID, hop.relayInfo,
3✔
257
                                constraints,
3✔
258
                        )
3✔
259
                }
3✔
260
                if err != nil {
3✔
UNCOV
261
                        return nil, err
×
UNCOV
262
                }
×
263

264
                hopDataSet = append(hopDataSet, info)
3✔
265
        }
266

267
        // Sort the hop info list in reverse order so that the data for the
268
        // introduction node is first.
269
        sort.Slice(hopDataSet, func(i, j int) bool {
6✔
270
                return j < i
3✔
271
        })
3✔
272

273
        // Add padding to each route data instance until the encrypted data
274
        // blobs are all the same size.
275
        paymentPath, _, err := padHopInfo(
3✔
276
                hopDataSet, true, record.AverageDummyHopPayloadSize,
3✔
277
        )
3✔
278
        if err != nil {
3✔
UNCOV
279
                return nil, err
×
UNCOV
280
        }
×
281

282
        // Derive an ephemeral session key.
283
        sessionKey, err := btcec.NewPrivateKey()
3✔
284
        if err != nil {
3✔
UNCOV
285
                return nil, err
×
UNCOV
286
        }
×
287

288
        // Encrypt the hop info.
289
        blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath)
3✔
290
        if err != nil {
3✔
UNCOV
291
                return nil, err
×
292
        }
×
293

294
        if len(blindedPath.BlindedHops) < 1 {
3✔
UNCOV
295
                return nil, fmt.Errorf("blinded path must have at least one " +
×
UNCOV
296
                        "hop")
×
UNCOV
297
        }
×
298

299
        // Overwrite the introduction point's blinded pub key with the real
300
        // pub key since then we can use this more compact format in the
301
        // invoice without needing to encode the un-used blinded node pub key of
302
        // the intro node.
303
        blindedPath.BlindedHops[0].BlindedNodePub =
3✔
304
                blindedPath.IntroductionPoint
3✔
305

3✔
306
        // Now construct a z32 blinded path.
3✔
307
        return &zpay32.BlindedPaymentPath{
3✔
308
                FeeBaseMsat:                 uint32(baseFee),
3✔
309
                FeeRate:                     feeRate,
3✔
310
                CltvExpiryDelta:             cltvDelta,
3✔
311
                HTLCMinMsat:                 uint64(minHTLC),
3✔
312
                HTLCMaxMsat:                 uint64(maxHTLC),
3✔
313
                Features:                    lnwire.EmptyFeatureVector(),
3✔
314
                FirstEphemeralBlindingPoint: blindedPath.BlindingPoint,
3✔
315
                Hops:                        blindedPath.BlindedHops,
3✔
316
        }, nil
3✔
317
}
318

319
// hopRelayInfo packages together the relay info to send to hop on a blinded
320
// path along with the pub key of that hop and the SCID that the hop should
321
// forward the payment on to.
322
type hopRelayInfo struct {
323
        hopPubKey      route.Vertex
324
        nextSCID       lnwire.ShortChannelID
325
        relayInfo      *record.PaymentRelayInfo
326
        nextHopIsDummy bool
327
}
328

329
// collectRelayInfo collects the relay policy rules for each relay hop on the
330
// route and applies any policy buffers.
331
//
332
// For the blinded route:
333
//
334
//        C --chan(CB)--> B --chan(BA)--> A
335
//
336
// where C is the introduction node, the route.Route struct we are given will
337
// have SourcePubKey set to C's pub key, and then it will have the following
338
// route.Hops:
339
//
340
//   - PubKeyBytes: B, ChannelID: chan(CB)
341
//   - PubKeyBytes: A, ChannelID: chan(BA)
342
//
343
// We, however, want to collect the channel policies for the following PubKey
344
// and ChannelID pairs:
345
//
346
//   - PubKey: C, ChannelID: chan(CB)
347
//   - PubKey: B, ChannelID: chan(BA)
348
//
349
// Therefore, when we go through the route and its hops to collect policies, our
350
// index for collecting public keys will be trailing that of the channel IDs by
351
// 1.
352
//
353
// For any dummy hops on the route, this function also decides what to use as
354
// policy values for the dummy hops. If there are other real hops, then the
355
// dummy hop policy values are derived by taking the average of the real
356
// policy values. If there are no real hops (in other words we are the
357
// introduction node), then we use some default routing values and we use the
358
// average of our channel capacities for the MaxHTLC value.
359
func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
360
        []*hopRelayInfo, lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
3✔
361

3✔
362
        var (
3✔
363
                // The first pub key is that of the introduction node.
3✔
364
                hopSource = path.introNode
3✔
365

3✔
366
                // A collection of the policy values of real hops on the path.
3✔
367
                policies = make(map[uint64]*BlindedHopPolicy)
3✔
368

3✔
369
                hasDummyHops bool
3✔
370
        )
3✔
371

3✔
372
        // On this first iteration, we just collect policy values of the real
3✔
373
        // hops on the path.
3✔
374
        for _, hop := range path.hops {
6✔
375
                // Once we have hit a dummy hop, all hops after will be dummy
3✔
376
                // hops too.
3✔
377
                if hop.isDummy {
6✔
378
                        hasDummyHops = true
3✔
379

3✔
380
                        break
3✔
381
                }
382

383
                // For real hops, retrieve the channel policy for this hop's
384
                // channel ID in the direction pointing away from the hopSource
385
                // node.
386
                policy, err := getNodeChannelPolicy(
3✔
387
                        cfg, hop.channelID, hopSource,
3✔
388
                )
3✔
389
                if err != nil {
3✔
UNCOV
390
                        return nil, 0, 0, err
×
UNCOV
391
                }
×
392

393
                policies[hop.channelID] = policy
3✔
394

3✔
395
                // This hop's pub key will be the policy creator for the next
3✔
396
                // hop.
3✔
397
                hopSource = hop.pubKey
3✔
398
        }
399

400
        var (
3✔
401
                dummyHopPolicy *BlindedHopPolicy
3✔
402
                err            error
3✔
403
        )
3✔
404

3✔
405
        // If the path does have dummy hops, we need to decide which policy
3✔
406
        // values to use for these hops.
3✔
407
        if hasDummyHops {
6✔
408
                dummyHopPolicy, err = computeDummyHopPolicy(
3✔
409
                        cfg.DefaultDummyHopPolicy, cfg.FetchOurOpenChannels,
3✔
410
                        policies,
3✔
411
                )
3✔
412
                if err != nil {
3✔
UNCOV
413
                        return nil, 0, 0, err
×
UNCOV
414
                }
×
415
        }
416

417
        // We iterate through the hops one more time. This time it is to
418
        // buffer the policy values, collect the payment relay info to send to
419
        // each hop, and to compute the min and max HTLC values for the path.
420
        var (
3✔
421
                hops    = make([]*hopRelayInfo, 0, len(path.hops))
3✔
422
                minHTLC lnwire.MilliSatoshi
3✔
423
                maxHTLC lnwire.MilliSatoshi
3✔
424
        )
3✔
425
        // The first pub key is that of the introduction node.
3✔
426
        hopSource = path.introNode
3✔
427
        for _, hop := range path.hops {
6✔
428
                var (
3✔
429
                        policy = dummyHopPolicy
3✔
430
                        ok     bool
3✔
431
                        err    error
3✔
432
                )
3✔
433

3✔
434
                if !hop.isDummy {
6✔
435
                        policy, ok = policies[hop.channelID]
3✔
436
                        if !ok {
3✔
437
                                return nil, 0, 0, fmt.Errorf("no cached "+
×
UNCOV
438
                                        "policy found for channel ID: %d",
×
UNCOV
439
                                        hop.channelID)
×
UNCOV
440
                        }
×
441
                }
442

443
                if policy.MinHTLCMsat > cfg.ValueMsat {
3✔
444
                        return nil, 0, 0, fmt.Errorf("%w: minHTLC of hop "+
×
445
                                "policy larger than payment amt: sentAmt(%v), "+
×
UNCOV
446
                                "minHTLC(%v)", errInvalidBlindedPath,
×
UNCOV
447
                                cfg.ValueMsat, policy.MinHTLCMsat)
×
UNCOV
448
                }
×
449

450
                bufferPolicy, err := cfg.AddPolicyBuffer(policy)
3✔
451
                if err != nil {
3✔
UNCOV
452
                        return nil, 0, 0, err
×
UNCOV
453
                }
×
454

455
                // We only use the new buffered policy if the new minHTLC value
456
                // does not violate the sender amount.
457
                //
458
                // NOTE: We don't check this for maxHTLC, because the payment
459
                // amount can always be splitted using MPP.
460
                if bufferPolicy.MinHTLCMsat <= cfg.ValueMsat {
6✔
461
                        policy = bufferPolicy
3✔
462
                }
3✔
463

464
                // If this is the first policy we are collecting, then use this
465
                // policy to set the base values for min/max htlc.
466
                if len(hops) == 0 {
6✔
467
                        minHTLC = policy.MinHTLCMsat
3✔
468
                        maxHTLC = policy.MaxHTLCMsat
3✔
469
                } else {
6✔
470
                        if policy.MinHTLCMsat > minHTLC {
3✔
UNCOV
471
                                minHTLC = policy.MinHTLCMsat
×
472
                        }
×
473

474
                        if policy.MaxHTLCMsat < maxHTLC {
3✔
UNCOV
475
                                maxHTLC = policy.MaxHTLCMsat
×
UNCOV
476
                        }
×
477
                }
478

479
                // From the policy values for this hop, we can collect the
480
                // payment relay info that we will send to this hop.
481
                hops = append(hops, &hopRelayInfo{
3✔
482
                        hopPubKey: hopSource,
3✔
483
                        nextSCID:  lnwire.NewShortChanIDFromInt(hop.channelID),
3✔
484
                        relayInfo: &record.PaymentRelayInfo{
3✔
485
                                FeeRate:         policy.FeeRate,
3✔
486
                                BaseFee:         policy.BaseFee,
3✔
487
                                CltvExpiryDelta: policy.CLTVExpiryDelta,
3✔
488
                        },
3✔
489
                        nextHopIsDummy: hop.isDummy,
3✔
490
                })
3✔
491

3✔
492
                // This hop's pub key will be the policy creator for the next
3✔
493
                // hop.
3✔
494
                hopSource = hop.pubKey
3✔
495
        }
496

497
        // It can happen that there is no HTLC-range overlap between the various
498
        // hops along the path. We return errInvalidBlindedPath to indicate that
499
        // this route was not usable
500
        if minHTLC > maxHTLC {
3✔
501
                return nil, 0, 0, fmt.Errorf("%w: resulting blinded path min "+
×
UNCOV
502
                        "HTLC value is larger than the resulting max HTLC "+
×
UNCOV
503
                        "value", errInvalidBlindedPath)
×
UNCOV
504
        }
×
505

506
        return hops, minHTLC, maxHTLC, nil
3✔
507
}
508

509
// buildDummyRouteData constructs the record.BlindedRouteData struct for the
510
// given a hop in a blinded route where the following hop is a dummy hop.
511
func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
512
        constraints *record.PaymentConstraints) (*hopData, error) {
3✔
513

3✔
514
        nodeID, err := btcec.ParsePubKey(node[:])
3✔
515
        if err != nil {
3✔
UNCOV
516
                return nil, err
×
UNCOV
517
        }
×
518

519
        return &hopData{
3✔
520
                data: record.NewDummyHopRouteData(
3✔
521
                        nodeID, *relayInfo, *constraints,
3✔
522
                ),
3✔
523
                nodeID: nodeID,
3✔
524
        }, nil
3✔
525
}
526

527
// computeDummyHopPolicy determines policy values to use for a dummy hop on a
528
// blinded path. If other real policy values exist, then we use the average of
529
// those values for the dummy hop policy values. Otherwise, in the case were
530
// there are no real policy values due to this node being the introduction node,
531
// we use the provided default policy values, and we get the average capacity of
532
// this node's channels to compute a MaxHTLC value.
533
func computeDummyHopPolicy(defaultPolicy *BlindedHopPolicy,
534
        fetchOurChannels func() ([]*channeldb.OpenChannel, error),
535
        policies map[uint64]*BlindedHopPolicy) (*BlindedHopPolicy, error) {
3✔
536

3✔
537
        numPolicies := len(policies)
3✔
538

3✔
539
        // If there are no real policies to calculate an average policy from,
3✔
540
        // then we use the default. The only thing we need to calculate here
3✔
541
        // though is the MaxHTLC value.
3✔
542
        if numPolicies == 0 {
6✔
543
                chans, err := fetchOurChannels()
3✔
544
                if err != nil {
3✔
UNCOV
545
                        return nil, err
×
546
                }
×
547

548
                if len(chans) == 0 {
3✔
UNCOV
549
                        return nil, fmt.Errorf("node has no channels to " +
×
UNCOV
550
                                "receive on")
×
UNCOV
551
                }
×
552

553
                // Calculate the average channel capacity and use this as the
554
                // MaxHTLC value.
555
                var maxHTLC btcutil.Amount
3✔
556
                for _, c := range chans {
6✔
557
                        maxHTLC += c.Capacity
3✔
558
                }
3✔
559

560
                maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
3✔
561

3✔
562
                return &BlindedHopPolicy{
3✔
563
                        CLTVExpiryDelta: defaultPolicy.CLTVExpiryDelta,
3✔
564
                        FeeRate:         defaultPolicy.FeeRate,
3✔
565
                        BaseFee:         defaultPolicy.BaseFee,
3✔
566
                        MinHTLCMsat:     defaultPolicy.MinHTLCMsat,
3✔
567
                        MaxHTLCMsat:     lnwire.NewMSatFromSatoshis(maxHTLC),
3✔
568
                }, nil
3✔
569
        }
570

571
        var avgPolicy BlindedHopPolicy
3✔
572

3✔
573
        for _, policy := range policies {
6✔
574
                avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
3✔
575
                avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
3✔
576
                avgPolicy.BaseFee += policy.BaseFee
3✔
577
                avgPolicy.FeeRate += policy.FeeRate
3✔
578
                avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
3✔
579
        }
3✔
580

581
        avgPolicy.MinHTLCMsat = lnwire.MilliSatoshi(
3✔
582
                float64(avgPolicy.MinHTLCMsat) / float64(numPolicies),
3✔
583
        )
3✔
584
        avgPolicy.MaxHTLCMsat = lnwire.MilliSatoshi(
3✔
585
                float64(avgPolicy.MaxHTLCMsat) / float64(numPolicies),
3✔
586
        )
3✔
587
        avgPolicy.BaseFee = lnwire.MilliSatoshi(
3✔
588
                float64(avgPolicy.BaseFee) / float64(numPolicies),
3✔
589
        )
3✔
590
        avgPolicy.FeeRate = uint32(
3✔
591
                float64(avgPolicy.FeeRate) / float64(numPolicies),
3✔
592
        )
3✔
593
        avgPolicy.CLTVExpiryDelta = uint16(
3✔
594
                float64(avgPolicy.CLTVExpiryDelta) / float64(numPolicies),
3✔
595
        )
3✔
596

3✔
597
        return &avgPolicy, nil
3✔
598
}
599

600
// buildHopRouteData constructs the record.BlindedRouteData struct for the given
601
// non-final hop on a blinded path and packages it with the node's ID.
602
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,
603
        relayInfo *record.PaymentRelayInfo,
604
        constraints *record.PaymentConstraints) (*hopData, error) {
3✔
605

3✔
606
        // Wrap up the data we want to send to this hop.
3✔
607
        blindedRouteHopData := record.NewNonFinalBlindedRouteData(
3✔
608
                scid, nil, *relayInfo, constraints, nil,
3✔
609
        )
3✔
610

3✔
611
        nodeID, err := btcec.ParsePubKey(node[:])
3✔
612
        if err != nil {
3✔
UNCOV
613
                return nil, err
×
UNCOV
614
        }
×
615

616
        return &hopData{
3✔
617
                data:   blindedRouteHopData,
3✔
618
                nodeID: nodeID,
3✔
619
        }, nil
3✔
620
}
621

622
// buildFinalHopRouteData constructs the record.BlindedRouteData struct for the
623
// final hop and packages it with the real node ID of the node it is intended
624
// for.
625
func buildFinalHopRouteData(node route.Vertex, pathID []byte,
626
        constraints *record.PaymentConstraints) (*hopData, error) {
3✔
627

3✔
628
        blindedRouteHopData := record.NewFinalHopBlindedRouteData(
3✔
629
                constraints, pathID,
3✔
630
        )
3✔
631
        nodeID, err := btcec.ParsePubKey(node[:])
3✔
632
        if err != nil {
3✔
UNCOV
633
                return nil, err
×
UNCOV
634
        }
×
635

636
        return &hopData{
3✔
637
                data:   blindedRouteHopData,
3✔
638
                nodeID: nodeID,
3✔
639
        }, nil
3✔
640
}
641

642
// getNodeChanPolicy fetches the routing policy info for the given channel and
643
// node pair.
644
func getNodeChannelPolicy(cfg *BuildBlindedPathCfg, chanID uint64,
645
        nodeID route.Vertex) (*BlindedHopPolicy, error) {
3✔
646

3✔
647
        // Attempt to fetch channel updates for the given channel. We will have
3✔
648
        // at most two updates for a given channel.
3✔
649
        _, update1, update2, err := cfg.FetchChannelEdgesByID(chanID)
3✔
650
        if err != nil {
3✔
UNCOV
651
                return nil, err
×
UNCOV
652
        }
×
653

654
        // Now we need to determine which of the updates was created by the
655
        // node in question. We know the update is the correct one if the
656
        // "ToNode" for the fetched policy is _not_ equal to the node ID in
657
        // question.
658
        var policy *models.ChannelEdgePolicy
3✔
659
        switch {
3✔
660
        case update1 != nil && !bytes.Equal(update1.ToNode[:], nodeID[:]):
3✔
661
                policy = update1
3✔
662

663
        case update2 != nil && !bytes.Equal(update2.ToNode[:], nodeID[:]):
3✔
664
                policy = update2
3✔
665

UNCOV
666
        default:
×
UNCOV
667
                return nil, fmt.Errorf("no channel updates found from node "+
×
UNCOV
668
                        "%s for channel %d", nodeID, chanID)
×
669
        }
670

671
        return &BlindedHopPolicy{
3✔
672
                CLTVExpiryDelta: policy.TimeLockDelta,
3✔
673
                FeeRate:         uint32(policy.FeeProportionalMillionths),
3✔
674
                BaseFee:         policy.FeeBaseMSat,
3✔
675
                MinHTLCMsat:     policy.MinHTLC,
3✔
676
                MaxHTLCMsat:     policy.MaxHTLC,
3✔
677
        }, nil
3✔
678
}
679

680
// candidatePath holds all the information about a route to this node that we
681
// need in order to build a blinded route.
682
type candidatePath struct {
683
        introNode   route.Vertex
684
        finalNodeID route.Vertex
685
        hops        []*blindedPathHop
686
}
687

688
// String returns a string representation of the candidatePath which can be
689
// useful for logging and debugging.
690
func (c *candidatePath) String() string {
3✔
691
        str := fmt.Sprintf("[%s (intro node)]", c.introNode)
3✔
692

3✔
693
        for _, hop := range c.hops {
6✔
694
                if hop.isDummy {
6✔
695
                        str += "--->[dummy hop]"
3✔
696
                        continue
3✔
697
                }
698

699
                str += fmt.Sprintf("--<%d>-->[%s]", hop.channelID, hop.pubKey)
3✔
700
        }
701

702
        return str
3✔
703
}
704

705
// padWithDummyHops will append n dummy hops to the candidatePath hop set. The
706
// pub key for the dummy hop will be the same as the pub key for the final hop
707
// of the path. That way, the final hop will be able to decrypt the data
708
// encrypted for each dummy hop.
709
func (c *candidatePath) padWithDummyHops(n uint8) {
3✔
710
        for len(c.hops) < int(n) {
6✔
711
                c.hops = append(c.hops, &blindedPathHop{
3✔
712
                        pubKey:  c.finalNodeID,
3✔
713
                        isDummy: true,
3✔
714
                })
3✔
715
        }
3✔
716
}
717

718
// blindedPathHop holds the information we need to know about a hop in a route
719
// in order to use it in the construction of a blinded path.
720
type blindedPathHop struct {
721
        // pubKey is the real pub key of a node on a blinded path.
722
        pubKey route.Vertex
723

724
        // channelID is the channel along which the previous hop should forward
725
        // their HTLC in order to reach this hop.
726
        channelID uint64
727

728
        // isDummy is true if this hop is an appended dummy hop.
729
        isDummy bool
730
}
731

732
// extractCandidatePath extracts the data it needs from the given route.Route in
733
// order to construct a candidatePath.
734
func extractCandidatePath(path *route.Route) *candidatePath {
3✔
735
        var (
3✔
736
                hops      = make([]*blindedPathHop, len(path.Hops))
3✔
737
                finalNode = path.SourcePubKey
3✔
738
        )
3✔
739
        for i, hop := range path.Hops {
6✔
740
                hops[i] = &blindedPathHop{
3✔
741
                        pubKey:    hop.PubKeyBytes,
3✔
742
                        channelID: hop.ChannelID,
3✔
743
                }
3✔
744

3✔
745
                if i == len(path.Hops)-1 {
6✔
746
                        finalNode = hop.PubKeyBytes
3✔
747
                }
3✔
748
        }
749

750
        return &candidatePath{
3✔
751
                introNode:   path.SourcePubKey,
3✔
752
                finalNodeID: finalNode,
3✔
753
                hops:        hops,
3✔
754
        }
3✔
755
}
756

757
// BlindedHopPolicy holds the set of relay policy values to use for a channel
758
// in a blinded path.
759
type BlindedHopPolicy struct {
760
        CLTVExpiryDelta uint16
761
        FeeRate         uint32
762
        BaseFee         lnwire.MilliSatoshi
763
        MinHTLCMsat     lnwire.MilliSatoshi
764
        MaxHTLCMsat     lnwire.MilliSatoshi
765
}
766

767
// AddPolicyBuffer constructs the bufferedChanPolicies for a path hop by taking
768
// its actual policy values and multiplying them by the given multipliers.
769
// The base fee, fee rate and minimum HTLC msat values are adjusted via the
770
// incMultiplier while the maximum HTLC msat value is adjusted via the
771
// decMultiplier. If adjustments of the HTLC values no longer make sense
772
// then the original HTLC value is used.
773
func AddPolicyBuffer(policy *BlindedHopPolicy, incMultiplier,
774
        decMultiplier float64) (*BlindedHopPolicy, error) {
3✔
775

3✔
776
        if incMultiplier < 1 {
3✔
UNCOV
777
                return nil, fmt.Errorf("blinded path policy increase " +
×
UNCOV
778
                        "multiplier must be greater than or equal to 1")
×
779
        }
×
780

781
        if decMultiplier < 0 || decMultiplier > 1 {
3✔
UNCOV
782
                return nil, fmt.Errorf("blinded path policy decrease " +
×
UNCOV
783
                        "multiplier must be in the range [0;1]")
×
UNCOV
784
        }
×
785

786
        var (
3✔
787
                minHTLCMsat = lnwire.MilliSatoshi(
3✔
788
                        float64(policy.MinHTLCMsat) * incMultiplier,
3✔
789
                )
3✔
790
                maxHTLCMsat = lnwire.MilliSatoshi(
3✔
791
                        float64(policy.MaxHTLCMsat) * decMultiplier,
3✔
792
                )
3✔
793
        )
3✔
794

3✔
795
        // Make sure the new minimum is not more than the original maximum.
3✔
796
        // If it is, then just stick to the original minimum.
3✔
797
        if minHTLCMsat > policy.MaxHTLCMsat {
3✔
UNCOV
798
                minHTLCMsat = policy.MinHTLCMsat
×
UNCOV
799
        }
×
800

801
        // Make sure the new maximum is not less than the original minimum.
802
        // If it is, then just stick to the original maximum.
803
        if maxHTLCMsat < policy.MinHTLCMsat {
3✔
UNCOV
804
                maxHTLCMsat = policy.MaxHTLCMsat
×
UNCOV
805
        }
×
806

807
        // Also ensure that the new htlc bounds make sense. If the new minimum
808
        // is greater than the new maximum, then just let both to their original
809
        // values.
810
        if minHTLCMsat > maxHTLCMsat {
3✔
UNCOV
811
                minHTLCMsat = policy.MinHTLCMsat
×
UNCOV
812
                maxHTLCMsat = policy.MaxHTLCMsat
×
UNCOV
813
        }
×
814

815
        return &BlindedHopPolicy{
3✔
816
                CLTVExpiryDelta: uint16(
3✔
817
                        float64(policy.CLTVExpiryDelta) * incMultiplier,
3✔
818
                ),
3✔
819
                FeeRate: uint32(
3✔
820
                        float64(policy.FeeRate) * incMultiplier,
3✔
821
                ),
3✔
822
                BaseFee: lnwire.MilliSatoshi(
3✔
823
                        float64(policy.BaseFee) * incMultiplier,
3✔
824
                ),
3✔
825
                MinHTLCMsat: minHTLCMsat,
3✔
826
                MaxHTLCMsat: maxHTLCMsat,
3✔
827
        }, nil
3✔
828
}
829

830
// calcBlindedPathPolicies computes the accumulated policy values for the path.
831
// These values include the total base fee, the total proportional fee and the
832
// total CLTV delta. This function assumes that all the passed relay infos have
833
// already been adjusted with a buffer to account for easy probing attacks.
834
func calcBlindedPathPolicies(relayInfo []*record.PaymentRelayInfo,
835
        ourMinFinalCLTVDelta uint16) (lnwire.MilliSatoshi, uint32, uint16) {
3✔
836

3✔
837
        var (
3✔
838
                totalFeeBase lnwire.MilliSatoshi
3✔
839
                totalFeeProp uint32
3✔
840
                totalCLTV    = ourMinFinalCLTVDelta
3✔
841
        )
3✔
842
        // Use the algorithms defined in BOLT 4 to calculate the accumulated
3✔
843
        // relay fees for the route:
3✔
844
        //nolint:ll
3✔
845
        // https://github.com/lightning/bolts/blob/db278ab9b2baa0b30cfe79fb3de39280595938d3/04-onion-routing.md?plain=1#L255
3✔
846
        for i := len(relayInfo) - 1; i >= 0; i-- {
6✔
847
                info := relayInfo[i]
3✔
848

3✔
849
                totalFeeBase = calcNextTotalBaseFee(
3✔
850
                        totalFeeBase, info.BaseFee, info.FeeRate,
3✔
851
                )
3✔
852

3✔
853
                totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
3✔
854

3✔
855
                totalCLTV += info.CltvExpiryDelta
3✔
856
        }
3✔
857

858
        return totalFeeBase, totalFeeProp, totalCLTV
3✔
859
}
860

861
// calcNextTotalBaseFee takes the current total accumulated base fee of a
862
// blinded path at hop `n` along with the fee rate and base fee of the hop at
863
// `n+1` and uses these to calculate the accumulated base fee at hop `n+1`.
864
func calcNextTotalBaseFee(currentTotal, hopBaseFee lnwire.MilliSatoshi,
865
        hopFeeRate uint32) lnwire.MilliSatoshi {
3✔
866

3✔
867
        numerator := (uint32(hopBaseFee) * oneMillion) +
3✔
868
                (uint32(currentTotal) * (oneMillion + hopFeeRate)) +
3✔
869
                oneMillion - 1
3✔
870

3✔
871
        return lnwire.MilliSatoshi(numerator / oneMillion)
3✔
872
}
3✔
873

874
// calculateNextTotalFeeRate takes the current total accumulated fee rate of a
875
// blinded path at hop `n` along with the fee rate of the hop at `n+1` and uses
876
// these to calculate the accumulated fee rate at hop `n+1`.
877
func calcNextTotalFeeRate(currentTotal, hopFeeRate uint32) uint32 {
3✔
878
        numerator := (currentTotal+hopFeeRate)*oneMillion +
3✔
879
                currentTotal*hopFeeRate + oneMillion - 1
3✔
880

3✔
881
        return numerator / oneMillion
3✔
882
}
3✔
883

884
// hopData packages the record.BlindedRouteData for a hop on a blinded path with
885
// the real node ID of that hop.
886
type hopData struct {
887
        data   *record.BlindedRouteData
888
        nodeID *btcec.PublicKey
889
}
890

891
// padStats can be used to keep track of various pieces of data that we collect
892
// during a call to padHopInfo. This is useful for logging and for test
893
// assertions.
894
type padStats struct {
895
        minPayloadSize  int
896
        maxPayloadSize  int
897
        finalPaddedSize int
898
        numIterations   int
899
}
900

901
// padHopInfo iterates over a set of record.BlindedRouteData and adds padding
902
// where needed until the resulting encrypted data blobs are all the same size.
903
// This may take a few iterations due to the fact that a TLV field is used to
904
// add this padding. For example, if we want to add a 1 byte padding to a
905
// record.BlindedRouteData when it does not yet have any padding, then adding
906
// a 1 byte padding will actually add 3 bytes due to the bytes required when
907
// adding the initial type and length bytes. However, on the next iteration if
908
// we again add just 1 byte, then only a single byte will be added. The same
909
// iteration is required for padding values on the BigSize encoding bucket
910
// edges. The number of iterations that this function takes is also returned for
911
// testing purposes. If prePad is true, then zero byte padding is added to each
912
// payload that does not yet have padding. This will save some iterations for
913
// the majority of cases. minSize can be used to specify a minimum size that all
914
// payloads should be.
915
func padHopInfo(hopInfo []*hopData, prePad bool, minSize int) (
916
        []*sphinx.HopInfo, *padStats, error) {
3✔
917

3✔
918
        var (
3✔
919
                paymentPath = make([]*sphinx.HopInfo, len(hopInfo))
3✔
920
                stats       = padStats{finalPaddedSize: minSize}
3✔
921
        )
3✔
922

3✔
923
        // Pre-pad each payload with zero byte padding (if it does not yet have
3✔
924
        // padding) to save a couple of iterations in the majority of cases.
3✔
925
        if prePad {
6✔
926
                for _, info := range hopInfo {
6✔
927
                        if info.data.Padding.IsSome() {
3✔
UNCOV
928
                                continue
×
929
                        }
930

931
                        info.data.PadBy(0)
3✔
932
                }
933
        }
934

935
        for {
6✔
936
                stats.numIterations++
3✔
937

3✔
938
                // On each iteration of the loop, we first determine the
3✔
939
                // current largest encoded data blob size. This will be the
3✔
940
                // size we aim to get the others to match.
3✔
941
                var (
3✔
942
                        maxLen = minSize
3✔
943
                        minLen = math.MaxInt8
3✔
944
                )
3✔
945
                for i, hop := range hopInfo {
6✔
946
                        plainText, err := record.EncodeBlindedRouteData(
3✔
947
                                hop.data,
3✔
948
                        )
3✔
949
                        if err != nil {
3✔
UNCOV
950
                                return nil, nil, err
×
UNCOV
951
                        }
×
952

953
                        if len(plainText) > maxLen {
6✔
954
                                maxLen = len(plainText)
3✔
955

3✔
956
                                // Update the stats to take note of this new
3✔
957
                                // max since this may be the final max that all
3✔
958
                                // payloads will be padded to.
3✔
959
                                stats.finalPaddedSize = maxLen
3✔
960
                        }
3✔
961
                        if len(plainText) < minLen {
6✔
962
                                minLen = len(plainText)
3✔
963
                        }
3✔
964

965
                        paymentPath[i] = &sphinx.HopInfo{
3✔
966
                                NodePub:   hop.nodeID,
3✔
967
                                PlainText: plainText,
3✔
968
                        }
3✔
969
                }
970

971
                // If this is our first iteration, then we take note of the min
972
                // and max lengths of the payloads pre-padding for logging
973
                // later.
974
                if stats.numIterations == 1 {
6✔
975
                        stats.minPayloadSize = minLen
3✔
976
                        stats.maxPayloadSize = maxLen
3✔
977
                }
3✔
978

979
                // Now we iterate over them again and determine which ones we
980
                // need to add padding to.
981
                var numEqual int
3✔
982
                for i, hop := range hopInfo {
6✔
983
                        plainText := paymentPath[i].PlainText
3✔
984

3✔
985
                        // If the plaintext length is equal to the desired
3✔
986
                        // length, then we can continue. We use numEqual to
3✔
987
                        // keep track of how many have the same length.
3✔
988
                        if len(plainText) == maxLen {
6✔
989
                                numEqual++
3✔
990

3✔
991
                                continue
3✔
992
                        }
993

994
                        // If we previously added padding to this hop, we keep
995
                        // the length of that initial padding too.
996
                        var existingPadding int
3✔
997
                        hop.data.Padding.WhenSome(
3✔
998
                                func(p tlv.RecordT[tlv.TlvType1, []byte]) {
6✔
999
                                        existingPadding = len(p.Val)
3✔
1000
                                },
3✔
1001
                        )
1002

1003
                        // Add some padding bytes to the hop.
1004
                        hop.data.PadBy(
3✔
1005
                                existingPadding + maxLen - len(plainText),
3✔
1006
                        )
3✔
1007
                }
1008

1009
                // If all the payloads have the same length, we can exit the
1010
                // loop.
1011
                if numEqual == len(hopInfo) {
6✔
1012
                        break
3✔
1013
                }
1014
        }
1015

1016
        log.Debugf("Finished padding %d blinded path payloads to %d bytes "+
3✔
1017
                "each where the pre-padded min and max sizes were %d and %d "+
3✔
1018
                "bytes respectively", len(hopInfo), stats.finalPaddedSize,
3✔
1019
                stats.minPayloadSize, stats.maxPayloadSize)
3✔
1020

3✔
1021
        return paymentPath, &stats, nil
3✔
1022
}
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