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

lightningnetwork / lnd / 12312390362

13 Dec 2024 08:44AM UTC coverage: 57.458% (+8.5%) from 48.92%
12312390362

Pull #9343

github

ellemouton
fn: rework the ContextGuard and add tests

In this commit, the ContextGuard struct is re-worked such that the
context that its new main WithCtx method provides is cancelled in sync
with a parent context being cancelled or with it's quit channel being
cancelled. Tests are added to assert the behaviour. In order for the
close of the quit channel to be consistent with the cancelling of the
derived context, the quit channel _must_ be contained internal to the
ContextGuard so that callers are only able to close the channel via the
exposed Quit method which will then take care to first cancel any
derived context that depend on the quit channel before returning.
Pull Request #9343: fn: expand the ContextGuard and add tests

101853 of 177264 relevant lines covered (57.46%)

24972.93 hits per line

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

83.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
        // 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✔
121
                return nil, err
×
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
        blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath)
4✔
287
        if err != nil {
4✔
288
                return nil, err
×
289
        }
×
290

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

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

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

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

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

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

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

6✔
366
                hasDummyHops bool
6✔
367
        )
6✔
368

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

2✔
377
                        break
2✔
378
                }
379

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

390
                policies[hop.channelID] = policy
6✔
391

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

397
        var (
4✔
398
                dummyHopPolicy *BlindedHopPolicy
4✔
399
                err            error
4✔
400
        )
4✔
401

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

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

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

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

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

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

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

471
                        if policy.MaxHTLCMsat < maxHTLC {
7✔
472
                                maxHTLC = policy.MaxHTLCMsat
×
473
                        }
×
474
                }
475

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

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

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

503
        return hops, minHTLC, maxHTLC, nil
4✔
504
}
505

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

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

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

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

2✔
534
        numPolicies := len(policies)
2✔
535

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

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

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

557
                maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
×
558

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

568
        var avgPolicy BlindedHopPolicy
2✔
569

2✔
570
        for _, policy := range policies {
6✔
571
                avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
4✔
572
                avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
4✔
573
                avgPolicy.BaseFee += policy.BaseFee
4✔
574
                avgPolicy.FeeRate += policy.FeeRate
4✔
575
                avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
4✔
576
        }
4✔
577

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

2✔
594
        return &avgPolicy, nil
2✔
595
}
596

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

699
        return str
×
700
}
701

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

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

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

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

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

10✔
742
                if i == len(path.Hops)-1 {
15✔
743
                        finalNode = hop.PubKeyBytes
5✔
744
                }
5✔
745
        }
746

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

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

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

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

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

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

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

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

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

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

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

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

12✔
846
                totalFeeBase = calcNextTotalBaseFee(
12✔
847
                        totalFeeBase, info.BaseFee, info.FeeRate,
12✔
848
                )
12✔
849

12✔
850
                totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
12✔
851

12✔
852
                totalCLTV += info.CltvExpiryDelta
12✔
853
        }
12✔
854

855
        return totalFeeBase, totalFeeProp, totalCLTV
5✔
856
}
857

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

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

12✔
868
        return lnwire.MilliSatoshi(numerator / oneMillion)
12✔
869
}
12✔
870

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

12✔
878
        return numerator / oneMillion
12✔
879
}
12✔
880

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

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

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

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

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

928
                        info.data.PadBy(0)
2,742✔
929
                }
930
        }
931

932
        for {
234✔
933
                stats.numIterations++
123✔
934

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

950
                        if len(plainText) > maxLen {
2,902✔
951
                                maxLen = len(plainText)
120✔
952

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

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

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

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

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

2,764✔
988
                                continue
2,764✔
989
                        }
990

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

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

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

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

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