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

lightningnetwork / lnd / 14743636896

29 Apr 2025 11:57PM UTC coverage: 69.034% (-0.001%) from 69.035%
14743636896

Pull #9334

github

web-flow
Merge 69152d035 into f21e3f3ee
Pull Request #9334: Use all valid routes during blinded path construction

108 of 115 new or added lines in 4 files covered. (93.91%)

66 existing lines in 15 files now uncovered.

133925 of 193998 relevant lines covered (69.03%)

22059.34 hits per line

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

88.89
/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
        // Routes to be used for the construction of blinded paths. These routes
36
        // will consist of real nodes advertising the route blinding feature
37
        // bit. They may be of various lengths and may even contain only a
38
        // single hop. Any route shorter than MinNumHops will be padded with
39
        // dummy hops during route construction.
40
        Routes []*route.Route
41

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

47
        // FetchOurOpenChannels fetches this node's set of open channels.
48
        FetchOurOpenChannels func() ([]*channeldb.OpenChannel, error)
49

50
        // BestHeight can be used to fetch the best block height that this node
51
        // is aware of.
52
        BestHeight func() (uint32, error)
53

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

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

70
        // ValueMsat is the payment amount in milli-satoshis that must be
71
        // routed. This will be used for selecting appropriate routes to use for
72
        // the blinded path.
73
        ValueMsat lnwire.MilliSatoshi
74

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

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

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

102
        // MaxNumPaths is the maximum number of blinded paths to select.
103
        MaxNumPaths uint8
104

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

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

8✔
119
        if len(cfg.Routes) == 0 {
8✔
UNCOV
120
                return nil, fmt.Errorf("could not find any routes to self to " +
×
121
                        "use for blinded route construction")
×
122
        }
×
123

124
        // Not every route returned will necessarily result in a usable blinded
125
        // path and so the number of paths returned might be less than the
126
        // number of real routes returned by FindRoutes above.
127
        paths := make([]*zpay32.BlindedPaymentPath, 0, min(len(cfg.Routes),
8✔
128
                int(cfg.MaxNumPaths)))
8✔
129

8✔
130
        // For each route returned, we will construct the associated blinded
8✔
131
        // payment path, until the maximum number of allowed paths is reached.
8✔
132
        for _, route := range cfg.Routes {
20✔
133
                if len(paths) >= int(cfg.MaxNumPaths) {
13✔
134
                        break
1✔
135
                }
136
                // Extract the information we need from the route.
137
                candidatePath := extractCandidatePath(route)
11✔
138

11✔
139
                // Pad the given route with dummy hops until the minimum number
11✔
140
                // of hops is met.
11✔
141
                candidatePath.padWithDummyHops(cfg.MinNumHops)
11✔
142

11✔
143
                path, err := buildBlindedPaymentPath(cfg, candidatePath)
11✔
144
                if errors.Is(err, errInvalidBlindedPath) {
11✔
145
                        log.Debugf("Not using route (%s) as a blinded path "+
×
146
                                "since it resulted in an invalid blinded path",
×
147
                                route)
×
148

×
149
                        continue
×
150
                } else if err != nil {
13✔
151
                        log.Errorf("Not using route (%s) as a blinded path: %v",
2✔
152
                                route, err)
2✔
153

2✔
154
                        continue
2✔
155
                }
156

157
                log.Debugf("Route selected for blinded path: %s", candidatePath)
9✔
158

9✔
159
                paths = append(paths, path)
9✔
160
        }
161

162
        if len(paths) == 0 {
8✔
163
                return nil, fmt.Errorf("could not build any blinded paths")
×
164
        }
×
165

166
        return paths, nil
8✔
167
}
168

169
// buildBlindedPaymentPath takes a route from an introduction node to this node
170
// and uses the given config to convert it into a blinded payment path.
171
func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) (
172
        *zpay32.BlindedPaymentPath, error) {
11✔
173

11✔
174
        hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path)
11✔
175
        if err != nil {
13✔
176
                return nil, fmt.Errorf("could not collect blinded path relay "+
2✔
177
                        "info: %w", err)
2✔
178
        }
2✔
179

180
        relayInfo := make([]*record.PaymentRelayInfo, len(hops))
9✔
181
        for i, hop := range hops {
26✔
182
                relayInfo[i] = hop.relayInfo
17✔
183
        }
17✔
184

185
        // Using the collected relay info, we can calculate the aggregated
186
        // policy values for the route.
187
        baseFee, feeRate, cltvDelta := calcBlindedPathPolicies(
9✔
188
                relayInfo, uint16(cfg.MinFinalCLTVExpiryDelta),
9✔
189
        )
9✔
190

9✔
191
        currentHeight, err := cfg.BestHeight()
9✔
192
        if err != nil {
9✔
193
                return nil, err
×
194
        }
×
195

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

9✔
207
        // Determine the maximum CLTV expiry for the destination node.
9✔
208
        cltvExpiry := currentHeight + cfg.BlocksUntilExpiry +
9✔
209
                cfg.MinFinalCLTVExpiryDelta
9✔
210

9✔
211
        constraints := &record.PaymentConstraints{
9✔
212
                MaxCltvExpiry:   cltvExpiry,
9✔
213
                HtlcMinimumMsat: minHTLC,
9✔
214
        }
9✔
215

9✔
216
        // If the blinded route has only a source node (introduction node) and
9✔
217
        // no hops, then the destination node is also the source node.
9✔
218
        finalHopPubKey := path.introNode
9✔
219
        if len(path.hops) > 0 {
17✔
220
                finalHopPubKey = path.hops[len(path.hops)-1].pubKey
8✔
221
        }
8✔
222

223
        // For the final hop, we only send it the path ID and payment
224
        // constraints.
225
        info, err := buildFinalHopRouteData(
9✔
226
                finalHopPubKey, cfg.PathID, constraints,
9✔
227
        )
9✔
228
        if err != nil {
9✔
229
                return nil, err
×
230
        }
×
231

232
        hopDataSet = append(hopDataSet, info)
9✔
233

9✔
234
        // Iterate through the remaining (non-final) hops, back to front.
9✔
235
        for i := len(hops) - 1; i >= 0; i-- {
26✔
236
                hop := hops[i]
17✔
237

17✔
238
                cltvExpiry += uint32(hop.relayInfo.CltvExpiryDelta)
17✔
239

17✔
240
                constraints = &record.PaymentConstraints{
17✔
241
                        MaxCltvExpiry:   cltvExpiry,
17✔
242
                        HtlcMinimumMsat: minHTLC,
17✔
243
                }
17✔
244

17✔
245
                var info *hopData
17✔
246
                if hop.nextHopIsDummy {
24✔
247
                        info, err = buildDummyRouteData(
7✔
248
                                hop.hopPubKey, hop.relayInfo, constraints,
7✔
249
                        )
7✔
250
                } else {
20✔
251
                        info, err = buildHopRouteData(
13✔
252
                                hop.hopPubKey, hop.nextSCID, hop.relayInfo,
13✔
253
                                constraints,
13✔
254
                        )
13✔
255
                }
13✔
256
                if err != nil {
17✔
257
                        return nil, err
×
258
                }
×
259

260
                hopDataSet = append(hopDataSet, info)
17✔
261
        }
262

263
        // Sort the hop info list in reverse order so that the data for the
264
        // introduction node is first.
265
        sort.Slice(hopDataSet, func(i, j int) bool {
41✔
266
                return j < i
32✔
267
        })
32✔
268

269
        // Add padding to each route data instance until the encrypted data
270
        // blobs are all the same size.
271
        paymentPath, _, err := padHopInfo(
9✔
272
                hopDataSet, true, record.AverageDummyHopPayloadSize,
9✔
273
        )
9✔
274
        if err != nil {
9✔
275
                return nil, err
×
276
        }
×
277

278
        // Derive an ephemeral session key.
279
        sessionKey, err := btcec.NewPrivateKey()
9✔
280
        if err != nil {
9✔
281
                return nil, err
×
282
        }
×
283

284
        // Encrypt the hop info.
285
        blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath)
9✔
286
        if err != nil {
9✔
287
                return nil, err
×
288
        }
×
289

290
        if len(blindedPath.BlindedHops) < 1 {
9✔
291
                return nil, fmt.Errorf("blinded path must have at least one " +
×
292
                        "hop")
×
293
        }
×
294

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

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

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

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

11✔
358
        var (
11✔
359
                // The first pub key is that of the introduction node.
11✔
360
                hopSource = path.introNode
11✔
361

11✔
362
                // A collection of the policy values of real hops on the path.
11✔
363
                policies = make(map[uint64]*BlindedHopPolicy)
11✔
364

11✔
365
                hasDummyHops bool
11✔
366
        )
11✔
367

11✔
368
        // On this first iteration, we just collect policy values of the real
11✔
369
        // hops on the path.
11✔
370
        for _, hop := range path.hops {
28✔
371
                // Once we have hit a dummy hop, all hops after will be dummy
17✔
372
                // hops too.
17✔
373
                if hop.isDummy {
22✔
374
                        hasDummyHops = true
5✔
375

5✔
376
                        break
5✔
377
                }
378

379
                // For real hops, retrieve the channel policy for this hop's
380
                // channel ID in the direction pointing away from the hopSource
381
                // node.
382
                policy, err := getNodeChannelPolicy(
15✔
383
                        cfg, hop.channelID, hopSource,
15✔
384
                )
15✔
385
                if err != nil {
17✔
386
                        return nil, 0, 0, err
2✔
387
                }
2✔
388

389
                policies[hop.channelID] = policy
13✔
390

13✔
391
                // This hop's pub key will be the policy creator for the next
13✔
392
                // hop.
13✔
393
                hopSource = hop.pubKey
13✔
394
        }
395

396
        var (
9✔
397
                dummyHopPolicy *BlindedHopPolicy
9✔
398
                err            error
9✔
399
        )
9✔
400

9✔
401
        // If the path does have dummy hops, we need to decide which policy
9✔
402
        // values to use for these hops.
9✔
403
        if hasDummyHops {
14✔
404
                dummyHopPolicy, err = computeDummyHopPolicy(
5✔
405
                        cfg.DefaultDummyHopPolicy, cfg.FetchOurOpenChannels,
5✔
406
                        policies,
5✔
407
                )
5✔
408
                if err != nil {
5✔
409
                        return nil, 0, 0, err
×
410
                }
×
411
        }
412

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

17✔
430
                if !hop.isDummy {
30✔
431
                        policy, ok = policies[hop.channelID]
13✔
432
                        if !ok {
13✔
433
                                return nil, 0, 0, fmt.Errorf("no cached "+
×
434
                                        "policy found for channel ID: %d",
×
435
                                        hop.channelID)
×
436
                        }
×
437
                }
438

439
                if policy.MinHTLCMsat > cfg.ValueMsat {
17✔
440
                        return nil, 0, 0, fmt.Errorf("%w: minHTLC of hop "+
×
441
                                "policy larger than payment amt: sentAmt(%v), "+
×
442
                                "minHTLC(%v)", errInvalidBlindedPath,
×
443
                                cfg.ValueMsat, policy.MinHTLCMsat)
×
444
                }
×
445

446
                bufferPolicy, err := cfg.AddPolicyBuffer(policy)
17✔
447
                if err != nil {
17✔
448
                        return nil, 0, 0, err
×
449
                }
×
450

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

460
                // If this is the first policy we are collecting, then use this
461
                // policy to set the base values for min/max htlc.
462
                if len(hops) == 0 {
25✔
463
                        minHTLC = policy.MinHTLCMsat
8✔
464
                        maxHTLC = policy.MaxHTLCMsat
8✔
465
                } else {
20✔
466
                        if policy.MinHTLCMsat > minHTLC {
12✔
467
                                minHTLC = policy.MinHTLCMsat
×
468
                        }
×
469

470
                        if policy.MaxHTLCMsat < maxHTLC {
12✔
471
                                maxHTLC = policy.MaxHTLCMsat
×
472
                        }
×
473
                }
474

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

17✔
488
                // This hop's pub key will be the policy creator for the next
17✔
489
                // hop.
17✔
490
                hopSource = hop.pubKey
17✔
491
        }
492

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

502
        return hops, minHTLC, maxHTLC, nil
9✔
503
}
504

505
// buildDummyRouteData constructs the record.BlindedRouteData struct for the
506
// given a hop in a blinded route where the following hop is a dummy hop.
507
func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
508
        constraints *record.PaymentConstraints) (*hopData, error) {
7✔
509

7✔
510
        nodeID, err := btcec.ParsePubKey(node[:])
7✔
511
        if err != nil {
7✔
512
                return nil, err
×
513
        }
×
514

515
        return &hopData{
7✔
516
                data: record.NewDummyHopRouteData(
7✔
517
                        nodeID, *relayInfo, *constraints,
7✔
518
                ),
7✔
519
                nodeID: nodeID,
7✔
520
        }, nil
7✔
521
}
522

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

5✔
533
        numPolicies := len(policies)
5✔
534

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

544
                if len(chans) == 0 {
3✔
545
                        return nil, fmt.Errorf("node has no channels to " +
×
546
                                "receive on")
×
547
                }
×
548

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

556
                maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
3✔
557

3✔
558
                return &BlindedHopPolicy{
3✔
559
                        CLTVExpiryDelta: defaultPolicy.CLTVExpiryDelta,
3✔
560
                        FeeRate:         defaultPolicy.FeeRate,
3✔
561
                        BaseFee:         defaultPolicy.BaseFee,
3✔
562
                        MinHTLCMsat:     defaultPolicy.MinHTLCMsat,
3✔
563
                        MaxHTLCMsat:     lnwire.NewMSatFromSatoshis(maxHTLC),
3✔
564
                }, nil
3✔
565
        }
566

567
        var avgPolicy BlindedHopPolicy
5✔
568

5✔
569
        for _, policy := range policies {
12✔
570
                avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
7✔
571
                avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
7✔
572
                avgPolicy.BaseFee += policy.BaseFee
7✔
573
                avgPolicy.FeeRate += policy.FeeRate
7✔
574
                avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
7✔
575
        }
7✔
576

577
        avgPolicy.MinHTLCMsat = lnwire.MilliSatoshi(
5✔
578
                float64(avgPolicy.MinHTLCMsat) / float64(numPolicies),
5✔
579
        )
5✔
580
        avgPolicy.MaxHTLCMsat = lnwire.MilliSatoshi(
5✔
581
                float64(avgPolicy.MaxHTLCMsat) / float64(numPolicies),
5✔
582
        )
5✔
583
        avgPolicy.BaseFee = lnwire.MilliSatoshi(
5✔
584
                float64(avgPolicy.BaseFee) / float64(numPolicies),
5✔
585
        )
5✔
586
        avgPolicy.FeeRate = uint32(
5✔
587
                float64(avgPolicy.FeeRate) / float64(numPolicies),
5✔
588
        )
5✔
589
        avgPolicy.CLTVExpiryDelta = uint16(
5✔
590
                float64(avgPolicy.CLTVExpiryDelta) / float64(numPolicies),
5✔
591
        )
5✔
592

5✔
593
        return &avgPolicy, nil
5✔
594
}
595

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

13✔
602
        // Wrap up the data we want to send to this hop.
13✔
603
        blindedRouteHopData := record.NewNonFinalBlindedRouteData(
13✔
604
                scid, nil, *relayInfo, constraints, nil,
13✔
605
        )
13✔
606

13✔
607
        nodeID, err := btcec.ParsePubKey(node[:])
13✔
608
        if err != nil {
13✔
609
                return nil, err
×
610
        }
×
611

612
        return &hopData{
13✔
613
                data:   blindedRouteHopData,
13✔
614
                nodeID: nodeID,
13✔
615
        }, nil
13✔
616
}
617

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

9✔
624
        blindedRouteHopData := record.NewFinalHopBlindedRouteData(
9✔
625
                constraints, pathID,
9✔
626
        )
9✔
627
        nodeID, err := btcec.ParsePubKey(node[:])
9✔
628
        if err != nil {
9✔
629
                return nil, err
×
630
        }
×
631

632
        return &hopData{
9✔
633
                data:   blindedRouteHopData,
9✔
634
                nodeID: nodeID,
9✔
635
        }, nil
9✔
636
}
637

638
// getNodeChanPolicy fetches the routing policy info for the given channel and
639
// node pair.
640
func getNodeChannelPolicy(cfg *BuildBlindedPathCfg, chanID uint64,
641
        nodeID route.Vertex) (*BlindedHopPolicy, error) {
15✔
642

15✔
643
        // Attempt to fetch channel updates for the given channel. We will have
15✔
644
        // at most two updates for a given channel.
15✔
645
        _, update1, update2, err := cfg.FetchChannelEdgesByID(chanID)
15✔
646
        if err != nil {
17✔
647
                return nil, err
2✔
648
        }
2✔
649

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

659
        case update2 != nil && !bytes.Equal(update2.ToNode[:], nodeID[:]):
3✔
660
                policy = update2
3✔
661

662
        default:
×
663
                return nil, fmt.Errorf("no channel updates found from node "+
×
664
                        "%s for channel %d", nodeID, chanID)
×
665
        }
666

667
        return &BlindedHopPolicy{
13✔
668
                CLTVExpiryDelta: policy.TimeLockDelta,
13✔
669
                FeeRate:         uint32(policy.FeeProportionalMillionths),
13✔
670
                BaseFee:         policy.FeeBaseMSat,
13✔
671
                MinHTLCMsat:     policy.MinHTLC,
13✔
672
                MaxHTLCMsat:     policy.MaxHTLC,
13✔
673
        }, nil
13✔
674
}
675

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

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

3✔
689
        for _, hop := range c.hops {
6✔
690
                if hop.isDummy {
6✔
691
                        str += "--->[dummy hop]"
3✔
692
                        continue
3✔
693
                }
694

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

698
        return str
3✔
699
}
700

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

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

720
        // channelID is the channel along which the previous hop should forward
721
        // their HTLC in order to reach this hop.
722
        channelID uint64
723

724
        // isDummy is true if this hop is an appended dummy hop.
725
        isDummy bool
726
}
727

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

17✔
741
                if i == len(path.Hops)-1 {
27✔
742
                        finalNode = hop.PubKeyBytes
10✔
743
                }
10✔
744
        }
745

746
        return &candidatePath{
11✔
747
                introNode:   path.SourcePubKey,
11✔
748
                finalNodeID: finalNode,
11✔
749
                hops:        hops,
11✔
750
        }
11✔
751
}
752

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

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

11✔
772
        if incMultiplier < 1 {
12✔
773
                return nil, fmt.Errorf("blinded path policy increase " +
1✔
774
                        "multiplier must be greater than or equal to 1")
1✔
775
        }
1✔
776

777
        if decMultiplier < 0 || decMultiplier > 1 {
12✔
778
                return nil, fmt.Errorf("blinded path policy decrease " +
2✔
779
                        "multiplier must be in the range [0;1]")
2✔
780
        }
2✔
781

782
        var (
8✔
783
                minHTLCMsat = lnwire.MilliSatoshi(
8✔
784
                        float64(policy.MinHTLCMsat) * incMultiplier,
8✔
785
                )
8✔
786
                maxHTLCMsat = lnwire.MilliSatoshi(
8✔
787
                        float64(policy.MaxHTLCMsat) * decMultiplier,
8✔
788
                )
8✔
789
        )
8✔
790

8✔
791
        // Make sure the new minimum is not more than the original maximum.
8✔
792
        // If it is, then just stick to the original minimum.
8✔
793
        if minHTLCMsat > policy.MaxHTLCMsat {
9✔
794
                minHTLCMsat = policy.MinHTLCMsat
1✔
795
        }
1✔
796

797
        // Make sure the new maximum is not less than the original minimum.
798
        // If it is, then just stick to the original maximum.
799
        if maxHTLCMsat < policy.MinHTLCMsat {
9✔
800
                maxHTLCMsat = policy.MaxHTLCMsat
1✔
801
        }
1✔
802

803
        // Also ensure that the new htlc bounds make sense. If the new minimum
804
        // is greater than the new maximum, then just let both to their original
805
        // values.
806
        if minHTLCMsat > maxHTLCMsat {
9✔
807
                minHTLCMsat = policy.MinHTLCMsat
1✔
808
                maxHTLCMsat = policy.MaxHTLCMsat
1✔
809
        }
1✔
810

811
        return &BlindedHopPolicy{
8✔
812
                CLTVExpiryDelta: uint16(
8✔
813
                        float64(policy.CLTVExpiryDelta) * incMultiplier,
8✔
814
                ),
8✔
815
                FeeRate: uint32(
8✔
816
                        float64(policy.FeeRate) * incMultiplier,
8✔
817
                ),
8✔
818
                BaseFee: lnwire.MilliSatoshi(
8✔
819
                        float64(policy.BaseFee) * incMultiplier,
8✔
820
                ),
8✔
821
                MinHTLCMsat: minHTLCMsat,
8✔
822
                MaxHTLCMsat: maxHTLCMsat,
8✔
823
        }, nil
8✔
824
}
825

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

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

19✔
845
                totalFeeBase = calcNextTotalBaseFee(
19✔
846
                        totalFeeBase, info.BaseFee, info.FeeRate,
19✔
847
                )
19✔
848

19✔
849
                totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
19✔
850

19✔
851
                totalCLTV += info.CltvExpiryDelta
19✔
852
        }
19✔
853

854
        return totalFeeBase, totalFeeProp, totalCLTV
10✔
855
}
856

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

19✔
863
        numerator := (uint32(hopBaseFee) * oneMillion) +
19✔
864
                (uint32(currentTotal) * (oneMillion + hopFeeRate)) +
19✔
865
                oneMillion - 1
19✔
866

19✔
867
        return lnwire.MilliSatoshi(numerator / oneMillion)
19✔
868
}
19✔
869

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

19✔
877
        return numerator / oneMillion
19✔
878
}
19✔
879

880
// hopData packages the record.BlindedRouteData for a hop on a blinded path with
881
// the real node ID of that hop.
882
type hopData struct {
883
        data   *record.BlindedRouteData
884
        nodeID *btcec.PublicKey
885
}
886

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

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

116✔
914
        var (
116✔
915
                paymentPath = make([]*sphinx.HopInfo, len(hopInfo))
116✔
916
                stats       = padStats{finalPaddedSize: minSize}
116✔
917
        )
116✔
918

116✔
919
        // Pre-pad each payload with zero byte padding (if it does not yet have
116✔
920
        // padding) to save a couple of iterations in the majority of cases.
116✔
921
        if prePad {
226✔
922
                for _, info := range hopInfo {
2,679✔
923
                        if info.data.Padding.IsSome() {
2,569✔
924
                                continue
×
925
                        }
926

927
                        info.data.PadBy(0)
2,569✔
928
                }
929
        }
930

931
        for {
246✔
932
                stats.numIterations++
130✔
933

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

949
                        if len(plainText) > maxLen {
2,738✔
950
                                maxLen = len(plainText)
123✔
951

123✔
952
                                // Update the stats to take note of this new
123✔
953
                                // max since this may be the final max that all
123✔
954
                                // payloads will be padded to.
123✔
955
                                stats.finalPaddedSize = maxLen
123✔
956
                        }
123✔
957
                        if len(plainText) < minLen {
2,748✔
958
                                minLen = len(plainText)
133✔
959
                        }
133✔
960

961
                        paymentPath[i] = &sphinx.HopInfo{
2,615✔
962
                                NodePub:   hop.nodeID,
2,615✔
963
                                PlainText: plainText,
2,615✔
964
                        }
2,615✔
965
                }
966

967
                // If this is our first iteration, then we take note of the min
968
                // and max lengths of the payloads pre-padding for logging
969
                // later.
970
                if stats.numIterations == 1 {
246✔
971
                        stats.minPayloadSize = minLen
116✔
972
                        stats.maxPayloadSize = maxLen
116✔
973
                }
116✔
974

975
                // Now we iterate over them again and determine which ones we
976
                // need to add padding to.
977
                var numEqual int
130✔
978
                for i, hop := range hopInfo {
2,745✔
979
                        plainText := paymentPath[i].PlainText
2,615✔
980

2,615✔
981
                        // If the plaintext length is equal to the desired
2,615✔
982
                        // length, then we can continue. We use numEqual to
2,615✔
983
                        // keep track of how many have the same length.
2,615✔
984
                        if len(plainText) == maxLen {
5,206✔
985
                                numEqual++
2,591✔
986

2,591✔
987
                                continue
2,591✔
988
                        }
989

990
                        // If we previously added padding to this hop, we keep
991
                        // the length of that initial padding too.
992
                        var existingPadding int
27✔
993
                        hop.data.Padding.WhenSome(
27✔
994
                                func(p tlv.RecordT[tlv.TlvType1, []byte]) {
51✔
995
                                        existingPadding = len(p.Val)
24✔
996
                                },
24✔
997
                        )
998

999
                        // Add some padding bytes to the hop.
1000
                        hop.data.PadBy(
27✔
1001
                                existingPadding + maxLen - len(plainText),
27✔
1002
                        )
27✔
1003
                }
1004

1005
                // If all the payloads have the same length, we can exit the
1006
                // loop.
1007
                if numEqual == len(hopInfo) {
246✔
1008
                        break
116✔
1009
                }
1010
        }
1011

1012
        log.Debugf("Finished padding %d blinded path payloads to %d bytes "+
116✔
1013
                "each where the pre-padded min and max sizes were %d and %d "+
116✔
1014
                "bytes respectively", len(hopInfo), stats.finalPaddedSize,
116✔
1015
                stats.minPayloadSize, stats.maxPayloadSize)
116✔
1016

116✔
1017
        return paymentPath, &stats, nil
116✔
1018
}
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