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

lightningnetwork / lnd / 13440912774

20 Feb 2025 05:14PM UTC coverage: 57.697% (-1.1%) from 58.802%
13440912774

Pull #9535

github

guggero
GitHub: remove duplicate caching

Turns out that actions/setup-go starting with @v4 also adds caching.
With that, our cache size on disk has almost doubled, leading to the
GitHub runner running out of space in certain situation.
We fix that by disabling the automated caching since we already have our
own, custom-tailored version.
Pull Request #9535: GitHub: remove duplicate caching

103519 of 179417 relevant lines covered (57.7%)

24825.3 hits per line

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

79.92
/routing/payment_session.go
1
package routing
2

3
import (
4
        "fmt"
5

6
        "github.com/btcsuite/btcd/btcec/v2"
7
        "github.com/btcsuite/btclog/v2"
8
        "github.com/lightningnetwork/lnd/channeldb"
9
        graphdb "github.com/lightningnetwork/lnd/graph/db"
10
        "github.com/lightningnetwork/lnd/graph/db/models"
11
        "github.com/lightningnetwork/lnd/lnutils"
12
        "github.com/lightningnetwork/lnd/lnwire"
13
        "github.com/lightningnetwork/lnd/netann"
14
        "github.com/lightningnetwork/lnd/routing/route"
15
)
16

17
// BlockPadding is used to increment the finalCltvDelta value for the last hop
18
// to prevent an HTLC being failed if some blocks are mined while it's in-flight.
19
const BlockPadding uint16 = 3
20

21
// ValidateCLTVLimit is a helper function that validates that the cltv limit is
22
// greater than the final cltv delta parameter, optionally including the
23
// BlockPadding in this calculation.
24
func ValidateCLTVLimit(limit uint32, delta uint16, includePad bool) error {
4✔
25
        if includePad {
6✔
26
                delta += BlockPadding
2✔
27
        }
2✔
28

29
        if limit <= uint32(delta) {
6✔
30
                return fmt.Errorf("cltv limit %v should be greater than %v",
2✔
31
                        limit, delta)
2✔
32
        }
2✔
33

34
        return nil
2✔
35
}
36

37
// noRouteError encodes a non-critical error encountered during path finding.
38
type noRouteError uint8
39

40
const (
41
        // errNoTlvPayload is returned when the destination hop does not support
42
        // a tlv payload.
43
        errNoTlvPayload noRouteError = iota
44

45
        // errNoPaymentAddr is returned when the destination hop does not
46
        // support payment addresses.
47
        errNoPaymentAddr
48

49
        // errNoPathFound is returned when a path to the target destination does
50
        // not exist in the graph.
51
        errNoPathFound
52

53
        // errInsufficientLocalBalance is returned when none of the local
54
        // channels have enough balance for the payment.
55
        errInsufficientBalance
56

57
        // errEmptyPaySession is returned when the empty payment session is
58
        // queried for a route.
59
        errEmptyPaySession
60

61
        // errUnknownRequiredFeature is returned when the destination node
62
        // requires an unknown feature.
63
        errUnknownRequiredFeature
64

65
        // errMissingDependentFeature is returned when the destination node
66
        // misses a feature that a feature that we require depends on.
67
        errMissingDependentFeature
68
)
69

70
var (
71
        // DefaultShardMinAmt is the default amount beyond which we won't try to
72
        // further split the payment if no route is found. It is the minimum
73
        // amount that we use as the shard size when splitting.
74
        DefaultShardMinAmt = lnwire.NewMSatFromSatoshis(10000)
75
)
76

77
// Error returns the string representation of the noRouteError.
78
func (e noRouteError) Error() string {
×
79
        switch e {
×
80
        case errNoTlvPayload:
×
81
                return "destination hop doesn't understand new TLV payloads"
×
82

83
        case errNoPaymentAddr:
×
84
                return "destination hop doesn't understand payment addresses"
×
85

86
        case errNoPathFound:
×
87
                return "unable to find a path to destination"
×
88

89
        case errEmptyPaySession:
×
90
                return "empty payment session"
×
91

92
        case errInsufficientBalance:
×
93
                return "insufficient local balance"
×
94

95
        case errUnknownRequiredFeature:
×
96
                return "unknown required feature"
×
97

98
        case errMissingDependentFeature:
×
99
                return "missing dependent feature"
×
100

101
        default:
×
102
                return "unknown no-route error"
×
103
        }
104
}
105

106
// FailureReason converts a path finding error into a payment-level failure.
107
func (e noRouteError) FailureReason() channeldb.FailureReason {
5✔
108
        switch e {
5✔
109
        case
110
                errNoTlvPayload,
111
                errNoPaymentAddr,
112
                errNoPathFound,
113
                errEmptyPaySession,
114
                errUnknownRequiredFeature,
115
                errMissingDependentFeature:
5✔
116

5✔
117
                return channeldb.FailureReasonNoRoute
5✔
118

119
        case errInsufficientBalance:
×
120
                return channeldb.FailureReasonInsufficientBalance
×
121

122
        default:
×
123
                return channeldb.FailureReasonError
×
124
        }
125
}
126

127
// PaymentSession is used during SendPayment attempts to provide routes to
128
// attempt. It also defines methods to give the PaymentSession additional
129
// information learned during the previous attempts.
130
type PaymentSession interface {
131
        // RequestRoute returns the next route to attempt for routing the
132
        // specified HTLC payment to the target node. The returned route should
133
        // carry at most maxAmt to the target node, and pay at most feeLimit in
134
        // fees. It can carry less if the payment is MPP. The activeShards
135
        // argument should be set to instruct the payment session about the
136
        // number of in flight HTLCS for the payment, such that it can choose
137
        // splitting strategy accordingly.
138
        //
139
        // A noRouteError is returned if a non-critical error is encountered
140
        // during path finding.
141
        RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
142
                activeShards, height uint32,
143
                firstHopCustomRecords lnwire.CustomRecords) (*route.Route,
144
                error)
145

146
        // UpdateAdditionalEdge takes an additional channel edge policy
147
        // (private channels) and applies the update from the message. Returns
148
        // a boolean to indicate whether the update has been applied without
149
        // error.
150
        UpdateAdditionalEdge(msg *lnwire.ChannelUpdate1,
151
                pubKey *btcec.PublicKey, policy *models.CachedEdgePolicy) bool
152

153
        // GetAdditionalEdgePolicy uses the public key and channel ID to query
154
        // the ephemeral channel edge policy for additional edges. Returns a nil
155
        // if nothing found.
156
        GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
157
                channelID uint64) *models.CachedEdgePolicy
158
}
159

160
// paymentSession is used during an HTLC routings session to prune the local
161
// chain view in response to failures, and also report those failures back to
162
// MissionController. The snapshot copied for this session will only ever grow,
163
// and will now be pruned after a decay like the main view within mission
164
// control. We do this as we want to avoid the case where we continually try a
165
// bad edge or route multiple times in a session. This can lead to an infinite
166
// loop if payment attempts take long enough. An additional set of edges can
167
// also be provided to assist in reaching the payment's destination.
168
type paymentSession struct {
169
        selfNode route.Vertex
170

171
        additionalEdges map[route.Vertex][]AdditionalEdge
172

173
        getBandwidthHints func(Graph) (bandwidthHints, error)
174

175
        payment *LightningPayment
176

177
        empty bool
178

179
        pathFinder pathFinder
180

181
        graphSessFactory GraphSessionFactory
182

183
        // pathFindingConfig defines global parameters that control the
184
        // trade-off in path finding between fees and probability.
185
        pathFindingConfig PathFindingConfig
186

187
        missionControl MissionControlQuerier
188

189
        // minShardAmt is the amount beyond which we won't try to further split
190
        // the payment if no route is found. If the maximum number of htlcs
191
        // specified in the payment is one, under no circumstances splitting
192
        // will happen and this value remains unused.
193
        minShardAmt lnwire.MilliSatoshi
194

195
        // log is a payment session-specific logger.
196
        log btclog.Logger
197
}
198

199
// newPaymentSession instantiates a new payment session.
200
func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
201
        getBandwidthHints func(Graph) (bandwidthHints, error),
202
        graphSessFactory GraphSessionFactory,
203
        missionControl MissionControlQuerier,
204
        pathFindingConfig PathFindingConfig) (*paymentSession, error) {
23✔
205

23✔
206
        edges, err := RouteHintsToEdges(p.RouteHints, p.Target)
23✔
207
        if err != nil {
23✔
208
                return nil, err
×
209
        }
×
210

211
        if p.BlindedPathSet != nil {
23✔
212
                if len(edges) != 0 {
×
213
                        return nil, fmt.Errorf("cannot have both route hints " +
×
214
                                "and blinded path")
×
215
                }
×
216

217
                edges, err = p.BlindedPathSet.ToRouteHints()
×
218
                if err != nil {
×
219
                        return nil, err
×
220
                }
×
221
        }
222

223
        logPrefix := fmt.Sprintf("PaymentSession(%x):", p.Identifier())
23✔
224

23✔
225
        return &paymentSession{
23✔
226
                selfNode:          selfNode,
23✔
227
                additionalEdges:   edges,
23✔
228
                getBandwidthHints: getBandwidthHints,
23✔
229
                payment:           p,
23✔
230
                pathFinder:        findPath,
23✔
231
                graphSessFactory:  graphSessFactory,
23✔
232
                pathFindingConfig: pathFindingConfig,
23✔
233
                missionControl:    missionControl,
23✔
234
                minShardAmt:       DefaultShardMinAmt,
23✔
235
                log:               log.WithPrefix(logPrefix),
23✔
236
        }, nil
23✔
237
}
238

239
// pathFindingError is a wrapper error type that is used to distinguish path
240
// finding errors from other errors in path finding loop.
241
type pathFindingError struct {
242
        error
243
}
244

245
// Unwrap returns the underlying error.
246
func (e *pathFindingError) Unwrap() error {
16✔
247
        return e.error
16✔
248
}
16✔
249

250
// RequestRoute returns a route which is likely to be capable for successfully
251
// routing the specified HTLC payment to the target node. Initially the first
252
// set of paths returned from this method may encounter routing failure along
253
// the way, however as more payments are sent, mission control will start to
254
// build an up to date view of the network itself. With each payment a new area
255
// will be explored, which feeds into the recommendations made for routing.
256
//
257
// NOTE: This function is safe for concurrent access.
258
// NOTE: Part of the PaymentSession interface.
259
func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
260
        activeShards, height uint32,
261
        firstHopCustomRecords lnwire.CustomRecords) (*route.Route, error) {
63✔
262

63✔
263
        if p.empty {
63✔
264
                return nil, errEmptyPaySession
×
265
        }
×
266

267
        // Add BlockPadding to the finalCltvDelta so that the receiving node
268
        // does not reject the HTLC if some blocks are mined while it's in-flight.
269
        finalCltvDelta := p.payment.FinalCLTVDelta
63✔
270
        finalCltvDelta += BlockPadding
63✔
271

63✔
272
        // We need to subtract the final delta before passing it into path
63✔
273
        // finding. The optimal path is independent of the final cltv delta and
63✔
274
        // the path finding algorithm is unaware of this value.
63✔
275
        cltvLimit := p.payment.CltvLimit - uint32(finalCltvDelta)
63✔
276

63✔
277
        // TODO(roasbeef): sync logic amongst dist sys
63✔
278

63✔
279
        // Taking into account this prune view, we'll attempt to locate a path
63✔
280
        // to our destination, respecting the recommendations from
63✔
281
        // MissionController.
63✔
282
        restrictions := &RestrictParams{
63✔
283
                ProbabilitySource:     p.missionControl.GetProbability,
63✔
284
                FeeLimit:              feeLimit,
63✔
285
                OutgoingChannelIDs:    p.payment.OutgoingChannelIDs,
63✔
286
                LastHop:               p.payment.LastHop,
63✔
287
                CltvLimit:             cltvLimit,
63✔
288
                DestCustomRecords:     p.payment.DestCustomRecords,
63✔
289
                DestFeatures:          p.payment.DestFeatures,
63✔
290
                PaymentAddr:           p.payment.PaymentAddr,
63✔
291
                Amp:                   p.payment.amp,
63✔
292
                Metadata:              p.payment.Metadata,
63✔
293
                FirstHopCustomRecords: firstHopCustomRecords,
63✔
294
        }
63✔
295

63✔
296
        finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
63✔
297

63✔
298
        // Before we enter the loop below, we'll make sure to respect the max
63✔
299
        // payment shard size (if it's set), which is effectively our
63✔
300
        // client-side MTU that we'll attempt to respect at all times.
63✔
301
        maxShardActive := p.payment.MaxShardAmt != nil
63✔
302
        if maxShardActive && maxAmt > *p.payment.MaxShardAmt {
65✔
303
                p.log.Debugf("Clamping payment attempt from %v to %v due to "+
2✔
304
                        "max shard size of %v", maxAmt, *p.payment.MaxShardAmt,
2✔
305
                        maxAmt)
2✔
306

2✔
307
                maxAmt = *p.payment.MaxShardAmt
2✔
308
        }
2✔
309

310
        var path []*unifiedEdge
63✔
311
        findPath := func(graph graphdb.NodeTraverser) error {
136✔
312
                // We'll also obtain a set of bandwidthHints from the lower
73✔
313
                // layer for each of our outbound channels. This will allow the
73✔
314
                // path finding to skip any links that aren't active or just
73✔
315
                // don't have enough bandwidth to carry the payment. New
73✔
316
                // bandwidth hints are queried for every new path finding
73✔
317
                // attempt, because concurrent payments may change balances.
73✔
318
                bandwidthHints, err := p.getBandwidthHints(graph)
73✔
319
                if err != nil {
73✔
320
                        return err
×
321
                }
×
322

323
                p.log.Debugf("pathfinding for amt=%v", maxAmt)
73✔
324

73✔
325
                // Find a route for the current amount.
73✔
326
                path, _, err = p.pathFinder(
73✔
327
                        &graphParams{
73✔
328
                                additionalEdges: p.additionalEdges,
73✔
329
                                bandwidthHints:  bandwidthHints,
73✔
330
                                graph:           graph,
73✔
331
                        },
73✔
332
                        restrictions, &p.pathFindingConfig,
73✔
333
                        p.selfNode, p.selfNode, p.payment.Target,
73✔
334
                        maxAmt, p.payment.TimePref, finalHtlcExpiry,
73✔
335
                )
73✔
336
                if err != nil {
89✔
337
                        // Wrap the error to distinguish path finding errors
16✔
338
                        // from other errors in this closure.
16✔
339
                        return &pathFindingError{err}
16✔
340
                }
16✔
341

342
                return nil
57✔
343
        }
344

345
        for {
136✔
346
                err := p.graphSessFactory.GraphSession(findPath)
73✔
347
                // If there is an error, and it is not a path finding error, we
73✔
348
                // return it immediately.
73✔
349
                if err != nil && !lnutils.ErrorAs[*pathFindingError](err) {
73✔
350
                        return nil, err
×
351
                } else if err != nil {
89✔
352
                        // If the error is a path finding error, we'll unwrap it
16✔
353
                        // to check the underlying error.
16✔
354
                        //
16✔
355
                        //nolint:errorlint
16✔
356
                        pErr, _ := err.(*pathFindingError)
16✔
357
                        err = pErr.Unwrap()
16✔
358
                }
16✔
359

360
                // Otherwise, we'll switch on the path finding error.
361
                switch {
73✔
362
                case err == errNoPathFound:
15✔
363
                        // Don't split if this is a legacy payment without mpp
15✔
364
                        // record. If it has a blinded path though, then we
15✔
365
                        // can split. Split payments to blinded paths won't have
15✔
366
                        // MPP records.
15✔
367
                        if p.payment.PaymentAddr.IsNone() &&
15✔
368
                                p.payment.BlindedPathSet == nil {
17✔
369

2✔
370
                                p.log.Debugf("not splitting because payment " +
2✔
371
                                        "address is unspecified")
2✔
372

2✔
373
                                return nil, errNoPathFound
2✔
374
                        }
2✔
375

376
                        if p.payment.DestFeatures == nil {
13✔
377
                                p.log.Debug("Not splitting because " +
×
378
                                        "destination DestFeatures is nil")
×
379
                                return nil, errNoPathFound
×
380
                        }
×
381

382
                        destFeatures := p.payment.DestFeatures
13✔
383
                        if !destFeatures.HasFeature(lnwire.MPPOptional) &&
13✔
384
                                !destFeatures.HasFeature(lnwire.AMPOptional) {
14✔
385

1✔
386
                                p.log.Debug("not splitting because " +
1✔
387
                                        "destination doesn't declare MPP or " +
1✔
388
                                        "AMP")
1✔
389

1✔
390
                                return nil, errNoPathFound
1✔
391
                        }
1✔
392

393
                        // No splitting if this is the last shard.
394
                        isLastShard := activeShards+1 >= p.payment.MaxParts
12✔
395
                        if isLastShard {
13✔
396
                                p.log.Debugf("not splitting because shard "+
1✔
397
                                        "limit %v has been reached",
1✔
398
                                        p.payment.MaxParts)
1✔
399

1✔
400
                                return nil, errNoPathFound
1✔
401
                        }
1✔
402

403
                        // This is where the magic happens. If we can't find a
404
                        // route, try it for half the amount.
405
                        maxAmt /= 2
11✔
406

11✔
407
                        // Put a lower bound on the minimum shard size.
11✔
408
                        if maxAmt < p.minShardAmt {
12✔
409
                                p.log.Debugf("not splitting because minimum "+
1✔
410
                                        "shard amount %v has been reached",
1✔
411
                                        p.minShardAmt)
1✔
412

1✔
413
                                return nil, errNoPathFound
1✔
414
                        }
1✔
415

416
                        // Go pathfinding.
417
                        continue
10✔
418

419
                // If there isn't enough local bandwidth, there is no point in
420
                // splitting. It won't be possible to create a complete set in
421
                // any case, but the sent out partial payments would be held by
422
                // the receiver until the mpp timeout.
423
                case err == errInsufficientBalance:
1✔
424
                        p.log.Debug("not splitting because local balance " +
1✔
425
                                "is insufficient")
1✔
426

1✔
427
                        return nil, err
1✔
428

429
                case err != nil:
×
430
                        return nil, err
×
431
                }
432

433
                // With the next candidate path found, we'll attempt to turn
434
                // this into a route by applying the time-lock and fee
435
                // requirements.
436
                route, err := newRoute(
57✔
437
                        p.selfNode, path, height,
57✔
438
                        finalHopParams{
57✔
439
                                amt:         maxAmt,
57✔
440
                                totalAmt:    p.payment.Amount,
57✔
441
                                cltvDelta:   finalCltvDelta,
57✔
442
                                records:     p.payment.DestCustomRecords,
57✔
443
                                paymentAddr: p.payment.PaymentAddr,
57✔
444
                                metadata:    p.payment.Metadata,
57✔
445
                        }, p.payment.BlindedPathSet,
57✔
446
                )
57✔
447
                if err != nil {
57✔
448
                        return nil, err
×
449
                }
×
450

451
                return route, err
57✔
452
        }
453
}
454

455
// UpdateAdditionalEdge updates the channel edge policy for a private edge. It
456
// validates the message signature and checks it's up to date, then applies the
457
// updates to the supplied policy. It returns a boolean to indicate whether
458
// there's an error when applying the updates.
459
func (p *paymentSession) UpdateAdditionalEdge(msg *lnwire.ChannelUpdate1,
460
        pubKey *btcec.PublicKey, policy *models.CachedEdgePolicy) bool {
3✔
461

3✔
462
        // Validate the message signature.
3✔
463
        if err := netann.VerifyChannelUpdateSignature(msg, pubKey); err != nil {
3✔
464
                log.Errorf(
×
465
                        "Unable to validate channel update signature: %v", err,
×
466
                )
×
467
                return false
×
468
        }
×
469

470
        // Update channel policy for the additional edge.
471
        policy.TimeLockDelta = msg.TimeLockDelta
3✔
472
        policy.FeeBaseMSat = lnwire.MilliSatoshi(msg.BaseFee)
3✔
473
        policy.FeeProportionalMillionths = lnwire.MilliSatoshi(msg.FeeRate)
3✔
474

3✔
475
        log.Debugf("New private channel update applied: %v",
3✔
476
                lnutils.SpewLogClosure(msg))
3✔
477

3✔
478
        return true
3✔
479
}
480

481
// GetAdditionalEdgePolicy uses the public key and channel ID to query the
482
// ephemeral channel edge policy for additional edges. Returns a nil if nothing
483
// found.
484
func (p *paymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
485
        channelID uint64) *models.CachedEdgePolicy {
5✔
486

5✔
487
        target := route.NewVertex(pubKey)
5✔
488

5✔
489
        edges, ok := p.additionalEdges[target]
5✔
490
        if !ok {
8✔
491
                return nil
3✔
492
        }
3✔
493

494
        for _, edge := range edges {
4✔
495
                policy := edge.EdgePolicy()
2✔
496
                if policy.ChannelID != channelID {
2✔
497
                        continue
×
498
                }
499

500
                return policy
2✔
501
        }
502

503
        return nil
×
504
}
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