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

lightningnetwork / lnd / 18016273007

25 Sep 2025 05:55PM UTC coverage: 54.653% (-12.0%) from 66.622%
18016273007

Pull #10248

github

web-flow
Merge 128443298 into b09b20c69
Pull Request #10248: Enforce TLV when creating a Route

25 of 30 new or added lines in 4 files covered. (83.33%)

23906 existing lines in 281 files now uncovered.

109536 of 200421 relevant lines covered (54.65%)

21816.97 hits per line

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

84.06
/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
        // FetchOurOpenChannels fetches this node's set of open channels.
49
        FetchOurOpenChannels func() ([]*channeldb.OpenChannel, error)
50

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

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

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

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

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

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

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

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

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

4✔
117
        // Find some appropriate routes for the value to be routed. This will
4✔
118
        // return a set of routes made up of real nodes.
4✔
119
        routes, err := cfg.FindRoutes(cfg.ValueMsat)
4✔
120
        if err != nil {
4✔
UNCOV
121
                return nil, err
×
UNCOV
122
        }
×
123

124
        if len(routes) == 0 {
4✔
125
                return nil, fmt.Errorf("could not find any routes to self to " +
×
126
                        "use for blinded route construction")
×
127
        }
×
128

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

4✔
134
        // For each route returned, we will construct the associated blinded
4✔
135
        // payment path.
4✔
136
        for _, route := range routes {
10✔
137
                // Extract the information we need from the route.
6✔
138
                candidatePath := extractCandidatePath(route)
6✔
139

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

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

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

2✔
155
                        continue
2✔
156
                }
157

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

4✔
160
                paths = append(paths, path)
4✔
161
        }
162

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

167
        return paths, nil
4✔
168
}
169

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

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

181
        relayInfo := make([]*record.PaymentRelayInfo, len(hops))
4✔
182
        for i, hop := range hops {
14✔
183
                relayInfo[i] = hop.relayInfo
10✔
184
        }
10✔
185

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

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

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

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

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

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

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

233
        hopDataSet = append(hopDataSet, info)
4✔
234

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

10✔
239
                cltvExpiry += uint32(hop.relayInfo.CltvExpiryDelta)
10✔
240

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

10✔
246
                var info *hopData
10✔
247
                if hop.nextHopIsDummy {
14✔
248
                        info, err = buildDummyRouteData(
4✔
249
                                hop.hopPubKey, hop.relayInfo, constraints,
4✔
250
                        )
4✔
251
                } else {
10✔
252
                        info, err = buildHopRouteData(
6✔
253
                                hop.hopPubKey, hop.nextSCID, hop.relayInfo,
6✔
254
                                constraints,
6✔
255
                        )
6✔
256
                }
6✔
257
                if err != nil {
10✔
258
                        return nil, err
×
259
                }
×
260

261
                hopDataSet = append(hopDataSet, info)
10✔
262
        }
263

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

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

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

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

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

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

4✔
304
        // The blinded path must support TLV payloads.
4✔
305
        relayFeatures := lnwire.EmptyFeatureVector()
4✔
306
        relayFeatures.Set(lnwire.TLVOnionPayloadOptional)
4✔
307

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

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

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

6✔
364
        var (
6✔
365
                // The first pub key is that of the introduction node.
6✔
366
                hopSource = path.introNode
6✔
367

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

6✔
371
                hasDummyHops bool
6✔
372
        )
6✔
373

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

2✔
382
                        break
2✔
383
                }
384

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

395
                policies[hop.channelID] = policy
6✔
396

6✔
397
                // This hop's pub key will be the policy creator for the next
6✔
398
                // hop.
6✔
399
                hopSource = hop.pubKey
6✔
400
        }
401

402
        var (
4✔
403
                dummyHopPolicy *BlindedHopPolicy
4✔
404
                err            error
4✔
405
        )
4✔
406

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

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

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

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

452
                bufferPolicy, err := cfg.AddPolicyBuffer(policy)
10✔
453
                if err != nil {
10✔
454
                        return nil, 0, 0, err
×
455
                }
×
456

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

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

476
                        if policy.MaxHTLCMsat < maxHTLC {
7✔
477
                                maxHTLC = policy.MaxHTLCMsat
×
478
                        }
×
479
                }
480

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

10✔
494
                // This hop's pub key will be the policy creator for the next
10✔
495
                // hop.
10✔
496
                hopSource = hop.pubKey
10✔
497
        }
498

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

508
        return hops, minHTLC, maxHTLC, nil
4✔
509
}
510

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

4✔
516
        nodeID, err := btcec.ParsePubKey(node[:])
4✔
517
        if err != nil {
4✔
518
                return nil, err
×
519
        }
×
520

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

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

2✔
539
        numPolicies := len(policies)
2✔
540

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

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

555
                // Calculate the average channel capacity and use this as the
556
                // MaxHTLC value.
UNCOV
557
                var maxHTLC btcutil.Amount
×
UNCOV
558
                for _, c := range chans {
×
UNCOV
559
                        maxHTLC += c.Capacity
×
UNCOV
560
                }
×
561

UNCOV
562
                maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
×
UNCOV
563

×
UNCOV
564
                return &BlindedHopPolicy{
×
UNCOV
565
                        CLTVExpiryDelta: defaultPolicy.CLTVExpiryDelta,
×
UNCOV
566
                        FeeRate:         defaultPolicy.FeeRate,
×
UNCOV
567
                        BaseFee:         defaultPolicy.BaseFee,
×
UNCOV
568
                        MinHTLCMsat:     defaultPolicy.MinHTLCMsat,
×
UNCOV
569
                        MaxHTLCMsat:     lnwire.NewMSatFromSatoshis(maxHTLC),
×
UNCOV
570
                }, nil
×
571
        }
572

573
        var avgPolicy BlindedHopPolicy
2✔
574

2✔
575
        for _, policy := range policies {
6✔
576
                avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
4✔
577
                avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
4✔
578
                avgPolicy.BaseFee += policy.BaseFee
4✔
579
                avgPolicy.FeeRate += policy.FeeRate
4✔
580
                avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
4✔
581
        }
4✔
582

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

2✔
599
        return &avgPolicy, nil
2✔
600
}
601

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

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

6✔
613
        nodeID, err := btcec.ParsePubKey(node[:])
6✔
614
        if err != nil {
6✔
615
                return nil, err
×
616
        }
×
617

618
        return &hopData{
6✔
619
                data:   blindedRouteHopData,
6✔
620
                nodeID: nodeID,
6✔
621
        }, nil
6✔
622
}
623

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

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

638
        return &hopData{
4✔
639
                data:   blindedRouteHopData,
4✔
640
                nodeID: nodeID,
4✔
641
        }, nil
4✔
642
}
643

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

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

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

UNCOV
665
        case update2 != nil && !bytes.Equal(update2.ToNode[:], nodeID[:]):
×
UNCOV
666
                policy = update2
×
667

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

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

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

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

×
UNCOV
695
        for _, hop := range c.hops {
×
UNCOV
696
                if hop.isDummy {
×
UNCOV
697
                        str += "--->[dummy hop]"
×
UNCOV
698
                        continue
×
699
                }
700

UNCOV
701
                str += fmt.Sprintf("--<%d>-->[%s]", hop.channelID, hop.pubKey)
×
702
        }
703

UNCOV
704
        return str
×
705
}
706

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

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

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

730
        // isDummy is true if this hop is an appended dummy hop.
731
        isDummy bool
732
}
733

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

10✔
747
                if i == len(path.Hops)-1 {
15✔
748
                        finalNode = hop.PubKeyBytes
5✔
749
                }
5✔
750
        }
751

752
        return &candidatePath{
6✔
753
                introNode:   path.SourcePubKey,
6✔
754
                finalNodeID: finalNode,
6✔
755
                hops:        hops,
6✔
756
        }
6✔
757
}
758

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

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

8✔
778
        if incMultiplier < 1 {
9✔
779
                return nil, fmt.Errorf("blinded path policy increase " +
1✔
780
                        "multiplier must be greater than or equal to 1")
1✔
781
        }
1✔
782

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

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

5✔
797
        // Make sure the new minimum is not more than the original maximum.
5✔
798
        // If it is, then just stick to the original minimum.
5✔
799
        if minHTLCMsat > policy.MaxHTLCMsat {
6✔
800
                minHTLCMsat = policy.MinHTLCMsat
1✔
801
        }
1✔
802

803
        // Make sure the new maximum is not less than the original minimum.
804
        // If it is, then just stick to the original maximum.
805
        if maxHTLCMsat < policy.MinHTLCMsat {
6✔
806
                maxHTLCMsat = policy.MaxHTLCMsat
1✔
807
        }
1✔
808

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

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

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

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

12✔
851
                totalFeeBase = calcNextTotalBaseFee(
12✔
852
                        totalFeeBase, info.BaseFee, info.FeeRate,
12✔
853
                )
12✔
854

12✔
855
                totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
12✔
856

12✔
857
                totalCLTV += info.CltvExpiryDelta
12✔
858
        }
12✔
859

860
        return totalFeeBase, totalFeeProp, totalCLTV
5✔
861
}
862

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

12✔
869
        numerator := (uint32(hopBaseFee) * oneMillion) +
12✔
870
                (uint32(currentTotal) * (oneMillion + hopFeeRate)) +
12✔
871
                oneMillion - 1
12✔
872

12✔
873
        return lnwire.MilliSatoshi(numerator / oneMillion)
12✔
874
}
12✔
875

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

12✔
883
        return numerator / oneMillion
12✔
884
}
12✔
885

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

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

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

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

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

933
                        info.data.PadBy(0)
2,469✔
934
                }
935
        }
936

937
        for {
234✔
938
                stats.numIterations++
123✔
939

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

955
                        if len(plainText) > maxLen {
2,630✔
956
                                maxLen = len(plainText)
121✔
957

121✔
958
                                // Update the stats to take note of this new
121✔
959
                                // max since this may be the final max that all
121✔
960
                                // payloads will be padded to.
121✔
961
                                stats.finalPaddedSize = maxLen
121✔
962
                        }
121✔
963
                        if len(plainText) < minLen {
2,634✔
964
                                minLen = len(plainText)
125✔
965
                        }
125✔
966

967
                        paymentPath[i] = &sphinx.HopInfo{
2,509✔
968
                                NodePub:   hop.nodeID,
2,509✔
969
                                PlainText: plainText,
2,509✔
970
                        }
2,509✔
971
                }
972

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

981
                // Now we iterate over them again and determine which ones we
982
                // need to add padding to.
983
                var numEqual int
123✔
984
                for i, hop := range hopInfo {
2,632✔
985
                        plainText := paymentPath[i].PlainText
2,509✔
986

2,509✔
987
                        // If the plaintext length is equal to the desired
2,509✔
988
                        // length, then we can continue. We use numEqual to
2,509✔
989
                        // keep track of how many have the same length.
2,509✔
990
                        if len(plainText) == maxLen {
5,000✔
991
                                numEqual++
2,491✔
992

2,491✔
993
                                continue
2,491✔
994
                        }
995

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

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

1011
                // If all the payloads have the same length, we can exit the
1012
                // loop.
1013
                if numEqual == len(hopInfo) {
234✔
1014
                        break
111✔
1015
                }
1016
        }
1017

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

111✔
1023
        return paymentPath, &stats, nil
111✔
1024
}
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