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

lightningnetwork / lnd / 13536249039

26 Feb 2025 03:42AM UTC coverage: 57.462% (-1.4%) from 58.835%
13536249039

Pull #8453

github

Roasbeef
peer: update chooseDeliveryScript to gen script if needed

In this commit, we update `chooseDeliveryScript` to generate a new
script if needed. This allows us to fold in a few other lines that
always followed this function into this expanded function.

The tests have been updated accordingly.
Pull Request #8453: [4/4] - multi: integrate new rbf coop close FSM into the existing peer flow

275 of 1318 new or added lines in 22 files covered. (20.86%)

19521 existing lines in 257 files now uncovered.

103858 of 180741 relevant lines covered (57.46%)

24750.23 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.
UNCOV
78
func (e noRouteError) Error() string {
×
UNCOV
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

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

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

UNCOV
92
        case errInsufficientBalance:
×
UNCOV
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

UNCOV
119
        case errInsufficientBalance:
×
UNCOV
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✔
UNCOV
212
                if len(edges) != 0 {
×
213
                        return nil, fmt.Errorf("cannot have both route hints " +
×
214
                                "and blinded path")
×
215
                }
×
216

UNCOV
217
                edges, err = p.BlindedPathSet.ToRouteHints()
×
UNCOV
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✔
UNCOV
264
                return nil, errEmptyPaySession
×
UNCOV
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