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

lightningnetwork / lnd / 13566028875

27 Feb 2025 12:09PM UTC coverage: 49.396% (-9.4%) from 58.748%
13566028875

Pull #9555

github

ellemouton
graph/db: populate the graph cache in Start instead of during construction

In this commit, we move the graph cache population logic out of the
ChannelGraph constructor and into its Start method instead.
Pull Request #9555: graph: extract cache from CRUD [6]

34 of 54 new or added lines in 4 files covered. (62.96%)

27464 existing lines in 436 files now uncovered.

101095 of 204664 relevant lines covered (49.4%)

1.54 hits per line

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

79.54
/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 {
3✔
25
        if includePad {
6✔
26
                delta += BlockPadding
3✔
27
        }
3✔
28

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

34
        return nil
3✔
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 {
3✔
79
        switch e {
3✔
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:
3✔
87
                return "unable to find a path to destination"
3✔
88

89
        case errEmptyPaySession:
3✔
90
                return "empty payment session"
3✔
91

92
        case errInsufficientBalance:
3✔
93
                return "insufficient local balance"
3✔
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 {
3✔
108
        switch e {
3✔
109
        case
110
                errNoTlvPayload,
111
                errNoPaymentAddr,
112
                errNoPathFound,
113
                errEmptyPaySession,
114
                errUnknownRequiredFeature,
115
                errMissingDependentFeature:
3✔
116

3✔
117
                return channeldb.FailureReasonNoRoute
3✔
118

119
        case errInsufficientBalance:
3✔
120
                return channeldb.FailureReasonInsufficientBalance
3✔
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) {
3✔
205

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

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

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

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

3✔
225
        return &paymentSession{
3✔
226
                selfNode:          selfNode,
3✔
227
                additionalEdges:   edges,
3✔
228
                getBandwidthHints: getBandwidthHints,
3✔
229
                payment:           p,
3✔
230
                pathFinder:        findPath,
3✔
231
                graphSessFactory:  graphSessFactory,
3✔
232
                pathFindingConfig: pathFindingConfig,
3✔
233
                missionControl:    missionControl,
3✔
234
                minShardAmt:       DefaultShardMinAmt,
3✔
235
                log:               log.WithPrefix(logPrefix),
3✔
236
        }, nil
3✔
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 {
3✔
247
        return e.error
3✔
248
}
3✔
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) {
3✔
262

3✔
263
        if p.empty {
6✔
264
                return nil, errEmptyPaySession
3✔
265
        }
3✔
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
3✔
270
        finalCltvDelta += BlockPadding
3✔
271

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

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

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

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

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

×
UNCOV
307
                maxAmt = *p.payment.MaxShardAmt
×
UNCOV
308
        }
×
309

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

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

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

342
                return nil
3✔
343
        }
344

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

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

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

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

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

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

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

×
UNCOV
390
                                return nil, errNoPathFound
×
UNCOV
391
                        }
×
392

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

3✔
400
                                return nil, errNoPathFound
3✔
401
                        }
3✔
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
3✔
406

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

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

416
                        // Go pathfinding.
417
                        continue
3✔
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:
3✔
424
                        p.log.Debug("not splitting because local balance " +
3✔
425
                                "is insufficient")
3✔
426

3✔
427
                        return nil, err
3✔
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(
3✔
437
                        p.selfNode, path, height,
3✔
438
                        finalHopParams{
3✔
439
                                amt:         maxAmt,
3✔
440
                                totalAmt:    p.payment.Amount,
3✔
441
                                cltvDelta:   finalCltvDelta,
3✔
442
                                records:     p.payment.DestCustomRecords,
3✔
443
                                paymentAddr: p.payment.PaymentAddr,
3✔
444
                                metadata:    p.payment.Metadata,
3✔
445
                        }, p.payment.BlindedPathSet,
3✔
446
                )
3✔
447
                if err != nil {
3✔
448
                        return nil, err
×
449
                }
×
450

451
                return route, err
3✔
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 {
3✔
486

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

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

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

500
                return policy
3✔
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