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

lightningnetwork / lnd / 13303171131

13 Feb 2025 08:10AM UTC coverage: 53.102% (+3.7%) from 49.366%
13303171131

Pull #9513

github

ellemouton
graph/db: unexport methods that take a transaction

Unexport and rename the methods that were previously used by the
graphsession package.
Pull Request #9513: graph+routing: refactor to remove `graphsession`

61 of 73 new or added lines in 7 files covered. (83.56%)

126 existing lines in 16 files now uncovered.

47503 of 89456 relevant lines covered (53.1%)

3.51 hits per line

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

78.74
/routing/payment_session.go
1
package routing
2

3
import (
4
        "errors"
5
        "fmt"
6

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

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

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

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

35
        return nil
3✔
36
}
37

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

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

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

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

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

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

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

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

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

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

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

87
        case errNoPathFound:
3✔
88
                return "unable to find a path to destination"
3✔
89

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

93
        case errInsufficientBalance:
3✔
94
                return "insufficient local balance"
3✔
95

96
        case errUnknownRequiredFeature:
×
UNCOV
97
                return "unknown required feature"
×
98

99
        case errMissingDependentFeature:
×
UNCOV
100
                return "missing dependent feature"
×
101

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

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

3✔
118
                return channeldb.FailureReasonNoRoute
3✔
119

120
        case errInsufficientBalance:
3✔
121
                return channeldb.FailureReasonInsufficientBalance
3✔
122

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

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

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

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

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

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

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

176
        payment *LightningPayment
177

178
        empty bool
179

180
        pathFinder pathFinder
181

182
        graphSessFactory GraphSessionFactory
183

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

188
        missionControl MissionControlQuerier
189

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

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

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

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

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

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

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

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

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

3✔
253
        if p.empty {
6✔
254
                return nil, errEmptyPaySession
3✔
255
        }
3✔
256

257
        // Add BlockPadding to the finalCltvDelta so that the receiving node
258
        // does not reject the HTLC if some blocks are mined while it's in-flight.
259
        finalCltvDelta := p.payment.FinalCLTVDelta
3✔
260
        finalCltvDelta += BlockPadding
3✔
261

3✔
262
        // We need to subtract the final delta before passing it into path
3✔
263
        // finding. The optimal path is independent of the final cltv delta and
3✔
264
        // the path finding algorithm is unaware of this value.
3✔
265
        cltvLimit := p.payment.CltvLimit - uint32(finalCltvDelta)
3✔
266

3✔
267
        // TODO(roasbeef): sync logic amongst dist sys
3✔
268

3✔
269
        // Taking into account this prune view, we'll attempt to locate a path
3✔
270
        // to our destination, respecting the recommendations from
3✔
271
        // MissionController.
3✔
272
        restrictions := &RestrictParams{
3✔
273
                ProbabilitySource:     p.missionControl.GetProbability,
3✔
274
                FeeLimit:              feeLimit,
3✔
275
                OutgoingChannelIDs:    p.payment.OutgoingChannelIDs,
3✔
276
                LastHop:               p.payment.LastHop,
3✔
277
                CltvLimit:             cltvLimit,
3✔
278
                DestCustomRecords:     p.payment.DestCustomRecords,
3✔
279
                DestFeatures:          p.payment.DestFeatures,
3✔
280
                PaymentAddr:           p.payment.PaymentAddr,
3✔
281
                Amp:                   p.payment.amp,
3✔
282
                Metadata:              p.payment.Metadata,
3✔
283
                FirstHopCustomRecords: firstHopCustomRecords,
3✔
284
        }
3✔
285

3✔
286
        finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
3✔
287

3✔
288
        // Before we enter the loop below, we'll make sure to respect the max
3✔
289
        // payment shard size (if it's set), which is effectively our
3✔
290
        // client-side MTU that we'll attempt to respect at all times.
3✔
291
        maxShardActive := p.payment.MaxShardAmt != nil
3✔
292
        if maxShardActive && maxAmt > *p.payment.MaxShardAmt {
3✔
UNCOV
293
                p.log.Debugf("Clamping payment attempt from %v to %v due to "+
×
UNCOV
294
                        "max shard size of %v", maxAmt, *p.payment.MaxShardAmt,
×
UNCOV
295
                        maxAmt)
×
UNCOV
296

×
UNCOV
297
                maxAmt = *p.payment.MaxShardAmt
×
UNCOV
298
        }
×
299

300
        var (
3✔
301
                // errPathFinding is used to distinguish path finding errors
3✔
302
                // from other errors in the below findPath closure.
3✔
303
                errPathFinding = fmt.Errorf("path finding error")
3✔
304
                path           []*unifiedEdge
3✔
305
        )
3✔
306
        findPath := func(graph graphdb.CachedGraph) error {
6✔
307
                // We'll also obtain a set of bandwidthHints from the lower
3✔
308
                // layer for each of our outbound channels. This will allow the
3✔
309
                // path finding to skip any links that aren't active or just
3✔
310
                // don't have enough bandwidth to carry the payment. New
3✔
311
                // bandwidth hints are queried for every new path finding
3✔
312
                // attempt, because concurrent payments may change balances.
3✔
313
                bandwidthHints, err := p.getBandwidthHints(graph)
3✔
314
                if err != nil {
3✔
UNCOV
315
                        return err
×
UNCOV
316
                }
×
317

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

3✔
320
                // Find a route for the current amount.
3✔
321
                path, _, err = p.pathFinder(
3✔
322
                        &graphParams{
3✔
323
                                additionalEdges: p.additionalEdges,
3✔
324
                                bandwidthHints:  bandwidthHints,
3✔
325
                                graph:           graph,
3✔
326
                        },
3✔
327
                        restrictions, &p.pathFindingConfig,
3✔
328
                        p.selfNode, p.selfNode, p.payment.Target,
3✔
329
                        maxAmt, p.payment.TimePref, finalHtlcExpiry,
3✔
330
                )
3✔
331
                if err != nil {
6✔
332
                        // Wrap the error to distinguish path finding errors
3✔
333
                        // from other errors in this closure.
3✔
334
                        return fmt.Errorf("%w: %w", errPathFinding, err)
3✔
335
                }
3✔
336

337
                return nil
3✔
338
        }
339

340
        for {
6✔
341
                err := p.graphSessFactory.GraphSession(findPath)
3✔
342
                // If there is an error, and it is not a path finding error, we
3✔
343
                // return it immediately.
3✔
344
                if err != nil && !errors.Is(err, errPathFinding) {
3✔
NEW
345
                        return nil, err
×
NEW
346
                }
×
347

348
                // Otherwise, we'll switch on the path finding error.
349
                switch {
3✔
350
                case errors.Is(err, errNoPathFound):
3✔
351
                        // Don't split if this is a legacy payment without mpp
3✔
352
                        // record. If it has a blinded path though, then we
3✔
353
                        // can split. Split payments to blinded paths won't have
3✔
354
                        // MPP records.
3✔
355
                        if p.payment.PaymentAddr.IsNone() &&
3✔
356
                                p.payment.BlindedPathSet == nil {
6✔
357

3✔
358
                                p.log.Debugf("not splitting because payment " +
3✔
359
                                        "address is unspecified")
3✔
360

3✔
361
                                return nil, errNoPathFound
3✔
362
                        }
3✔
363

364
                        if p.payment.DestFeatures == nil {
3✔
UNCOV
365
                                p.log.Debug("Not splitting because " +
×
UNCOV
366
                                        "destination DestFeatures is nil")
×
UNCOV
367
                                return nil, errNoPathFound
×
UNCOV
368
                        }
×
369

370
                        destFeatures := p.payment.DestFeatures
3✔
371
                        if !destFeatures.HasFeature(lnwire.MPPOptional) &&
3✔
372
                                !destFeatures.HasFeature(lnwire.AMPOptional) {
3✔
UNCOV
373

×
UNCOV
374
                                p.log.Debug("not splitting because " +
×
UNCOV
375
                                        "destination doesn't declare MPP or " +
×
UNCOV
376
                                        "AMP")
×
377

×
378
                                return nil, errNoPathFound
×
379
                        }
×
380

381
                        // No splitting if this is the last shard.
382
                        isLastShard := activeShards+1 >= p.payment.MaxParts
3✔
383
                        if isLastShard {
6✔
384
                                p.log.Debugf("not splitting because shard "+
3✔
385
                                        "limit %v has been reached",
3✔
386
                                        p.payment.MaxParts)
3✔
387

3✔
388
                                return nil, errNoPathFound
3✔
389
                        }
3✔
390

391
                        // This is where the magic happens. If we can't find a
392
                        // route, try it for half the amount.
393
                        maxAmt /= 2
3✔
394

3✔
395
                        // Put a lower bound on the minimum shard size.
3✔
396
                        if maxAmt < p.minShardAmt {
6✔
397
                                p.log.Debugf("not splitting because minimum "+
3✔
398
                                        "shard amount %v has been reached",
3✔
399
                                        p.minShardAmt)
3✔
400

3✔
401
                                return nil, errNoPathFound
3✔
402
                        }
3✔
403

404
                        // Go pathfinding.
405
                        continue
3✔
406

407
                // If there isn't enough local bandwidth, there is no point in
408
                // splitting. It won't be possible to create a complete set in
409
                // any case, but the sent out partial payments would be held by
410
                // the receiver until the mpp timeout.
411
                case errors.Is(err, errInsufficientBalance):
3✔
412
                        p.log.Debug("not splitting because local balance " +
3✔
413
                                "is insufficient")
3✔
414

3✔
415
                        return nil, err
3✔
416

UNCOV
417
                case err != nil:
×
UNCOV
418
                        return nil, err
×
419
                }
420

421
                // With the next candidate path found, we'll attempt to turn
422
                // this into a route by applying the time-lock and fee
423
                // requirements.
424
                route, err := newRoute(
3✔
425
                        p.selfNode, path, height,
3✔
426
                        finalHopParams{
3✔
427
                                amt:         maxAmt,
3✔
428
                                totalAmt:    p.payment.Amount,
3✔
429
                                cltvDelta:   finalCltvDelta,
3✔
430
                                records:     p.payment.DestCustomRecords,
3✔
431
                                paymentAddr: p.payment.PaymentAddr,
3✔
432
                                metadata:    p.payment.Metadata,
3✔
433
                        }, p.payment.BlindedPathSet,
3✔
434
                )
3✔
435
                if err != nil {
3✔
UNCOV
436
                        return nil, err
×
UNCOV
437
                }
×
438

439
                return route, err
3✔
440
        }
441
}
442

443
// UpdateAdditionalEdge updates the channel edge policy for a private edge. It
444
// validates the message signature and checks it's up to date, then applies the
445
// updates to the supplied policy. It returns a boolean to indicate whether
446
// there's an error when applying the updates.
447
func (p *paymentSession) UpdateAdditionalEdge(msg *lnwire.ChannelUpdate1,
448
        pubKey *btcec.PublicKey, policy *models.CachedEdgePolicy) bool {
3✔
449

3✔
450
        // Validate the message signature.
3✔
451
        if err := netann.VerifyChannelUpdateSignature(msg, pubKey); err != nil {
3✔
UNCOV
452
                log.Errorf(
×
UNCOV
453
                        "Unable to validate channel update signature: %v", err,
×
UNCOV
454
                )
×
UNCOV
455
                return false
×
UNCOV
456
        }
×
457

458
        // Update channel policy for the additional edge.
459
        policy.TimeLockDelta = msg.TimeLockDelta
3✔
460
        policy.FeeBaseMSat = lnwire.MilliSatoshi(msg.BaseFee)
3✔
461
        policy.FeeProportionalMillionths = lnwire.MilliSatoshi(msg.FeeRate)
3✔
462

3✔
463
        log.Debugf("New private channel update applied: %v",
3✔
464
                lnutils.SpewLogClosure(msg))
3✔
465

3✔
466
        return true
3✔
467
}
468

469
// GetAdditionalEdgePolicy uses the public key and channel ID to query the
470
// ephemeral channel edge policy for additional edges. Returns a nil if nothing
471
// found.
472
func (p *paymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
473
        channelID uint64) *models.CachedEdgePolicy {
3✔
474

3✔
475
        target := route.NewVertex(pubKey)
3✔
476

3✔
477
        edges, ok := p.additionalEdges[target]
3✔
478
        if !ok {
6✔
479
                return nil
3✔
480
        }
3✔
481

482
        for _, edge := range edges {
6✔
483
                policy := edge.EdgePolicy()
3✔
484
                if policy.ChannelID != channelID {
3✔
UNCOV
485
                        continue
×
486
                }
487

488
                return policy
3✔
489
        }
490

UNCOV
491
        return nil
×
492
}
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