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

lightningnetwork / lnd / 13211764208

08 Feb 2025 03:08AM UTC coverage: 49.288% (-9.5%) from 58.815%
13211764208

Pull #9489

github

calvinrzachman
itest: verify switchrpc server enforces send then track

We prevent the rpc server from allowing onion dispatches for
attempt IDs which have already been tracked by rpc clients.

This helps protect the client from leaking a duplicate onion
attempt. NOTE: This is not the only method for solving this
issue! The issue could be addressed via careful client side
programming which accounts for the uncertainty and async
nature of dispatching onions to a remote process via RPC.
This would require some lnd ChannelRouter changes for how
we intend to use these RPCs though.
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

474 of 990 new or added lines in 11 files covered. (47.88%)

27321 existing lines in 435 files now uncovered.

101192 of 205306 relevant lines covered (49.29%)

1.54 hits per line

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

85.96
/routing/pathfind.go
1
package routing
2

3
import (
4
        "bytes"
5
        "container/heap"
6
        "errors"
7
        "fmt"
8
        "math"
9
        "sort"
10
        "time"
11

12
        "github.com/btcsuite/btcd/btcutil"
13
        sphinx "github.com/lightningnetwork/lightning-onion"
14
        "github.com/lightningnetwork/lnd/feature"
15
        "github.com/lightningnetwork/lnd/fn/v2"
16
        graphdb "github.com/lightningnetwork/lnd/graph/db"
17
        "github.com/lightningnetwork/lnd/graph/db/models"
18
        "github.com/lightningnetwork/lnd/lnutils"
19
        "github.com/lightningnetwork/lnd/lnwire"
20
        "github.com/lightningnetwork/lnd/record"
21
        "github.com/lightningnetwork/lnd/routing/route"
22
)
23

24
const (
25
        // infinity is used as a starting distance in our shortest path search.
26
        infinity = math.MaxInt64
27

28
        // RiskFactorBillionths controls the influence of time lock delta
29
        // of a channel on route selection. It is expressed as billionths
30
        // of msat per msat sent through the channel per time lock delta
31
        // block. See edgeWeight function below for more details.
32
        // The chosen value is based on the previous incorrect weight function
33
        // 1 + timelock + fee * fee. In this function, the fee penalty
34
        // diminishes the time lock penalty for all but the smallest amounts.
35
        // To not change the behaviour of path finding too drastically, a
36
        // relatively small value is chosen which is still big enough to give
37
        // some effect with smaller time lock values. The value may need
38
        // tweaking and/or be made configurable in the future.
39
        RiskFactorBillionths = 15
40

41
        // estimatedNodeCount is used to preallocate the path finding structures
42
        // to avoid resizing and copies. It should be number on the same order as
43
        // the number of active nodes in the network.
44
        estimatedNodeCount = 10000
45

46
        // fakeHopHintCapacity is the capacity we assume for hop hint channels.
47
        // This is a high number, which expresses that a hop hint channel should
48
        // be able to route payments.
49
        fakeHopHintCapacity = btcutil.Amount(10 * btcutil.SatoshiPerBitcoin)
50
)
51

52
// pathFinder defines the interface of a path finding algorithm.
53
type pathFinder = func(g *graphParams, r *RestrictParams,
54
        cfg *PathFindingConfig, self, source, target route.Vertex,
55
        amt lnwire.MilliSatoshi, timePref float64, finalHtlcExpiry int32) (
56
        []*unifiedEdge, float64, error)
57

58
var (
59
        // DefaultEstimator is the default estimator used for computing
60
        // probabilities in pathfinding.
61
        DefaultEstimator = AprioriEstimatorName
62

63
        // DefaultAttemptCost is the default fixed virtual cost in path finding
64
        // of a failed payment attempt. It is used to trade off potentially
65
        // better routes against their probability of succeeding.
66
        DefaultAttemptCost = lnwire.NewMSatFromSatoshis(100)
67

68
        // DefaultAttemptCostPPM is the default proportional virtual cost in
69
        // path finding weight units of executing a payment attempt that fails.
70
        // It is used to trade off potentially better routes against their
71
        // probability of succeeding. This parameter is expressed in parts per
72
        // million of the payment amount.
73
        //
74
        // It is impossible to pick a perfect default value. The current value
75
        // of 0.1% is based on the idea that a transaction fee of 1% is within
76
        // reasonable territory and that a payment shouldn't need more than 10
77
        // attempts.
78
        DefaultAttemptCostPPM = int64(1000)
79

80
        // DefaultMinRouteProbability is the default minimum probability for routes
81
        // returned from findPath.
82
        DefaultMinRouteProbability = float64(0.01)
83

84
        // DefaultAprioriHopProbability is the default a priori probability for
85
        // a hop.
86
        DefaultAprioriHopProbability = float64(0.6)
87
)
88

89
// edgePolicyWithSource is a helper struct to keep track of the source node
90
// of a channel edge. ChannelEdgePolicy only contains to destination node
91
// of the edge.
92
type edgePolicyWithSource struct {
93
        sourceNode route.Vertex
94
        edge       AdditionalEdge
95
}
96

97
// finalHopParams encapsulates various parameters for route construction that
98
// apply to the final hop in a route. These features include basic payment data
99
// such as amounts and cltvs, as well as more complex features like destination
100
// custom records and payment address.
101
type finalHopParams struct {
102
        amt      lnwire.MilliSatoshi
103
        totalAmt lnwire.MilliSatoshi
104

105
        // cltvDelta is the final hop's minimum CLTV expiry delta.
106
        //
107
        // NOTE that in the case of paying to a blinded path, this value will
108
        // be set to a duplicate of the blinded path's accumulated CLTV value.
109
        // We would then only need to use this value in the case where the
110
        // introduction node of the path is also the destination node.
111
        cltvDelta uint16
112

113
        records     record.CustomSet
114
        paymentAddr fn.Option[[32]byte]
115

116
        // metadata is additional data that is sent along with the payment to
117
        // the payee.
118
        metadata []byte
119
}
120

121
// newRoute constructs a route using the provided path and final hop constraints.
122
// Any destination specific fields from the final hop params  will be attached
123
// assuming the destination's feature vector signals support, otherwise this
124
// method will fail.  If the route is too long, or the selected path cannot
125
// support the fully payment including fees, then a non-nil error is returned.
126
// If the route is to a blinded path, the blindedPath parameter is used to
127
// back fill additional fields that are required for a blinded payment. This is
128
// done in a separate pass to keep our route construction simple, as blinded
129
// paths require zero expiry and amount values for intermediate hops (which
130
// makes calculating the totals during route construction difficult if we
131
// include blinded paths on the first pass).
132
//
133
// NOTE: The passed slice of unified edges MUST be sorted in forward order: from
134
// the source to the target node of the path finding attempt. It is assumed that
135
// any feature vectors on all hops have been validated for transitive
136
// dependencies.
137
// NOTE: If a non-nil blinded path is provided it is assumed to have been
138
// validated by the caller.
139
func newRoute(sourceVertex route.Vertex,
140
        pathEdges []*unifiedEdge, currentHeight uint32, finalHop finalHopParams,
141
        blindedPathSet *BlindedPaymentPathSet) (*route.Route, error) {
3✔
142

3✔
143
        var (
3✔
144
                hops []*route.Hop
3✔
145

3✔
146
                // totalTimeLock will accumulate the cumulative time lock
3✔
147
                // across the entire route. This value represents how long the
3✔
148
                // sender will need to wait in the *worst* case.
3✔
149
                totalTimeLock = currentHeight
3✔
150

3✔
151
                // nextIncomingAmount is the amount that will need to flow into
3✔
152
                // the *next* hop. Since we're going to be walking the route
3✔
153
                // backwards below, this next hop gets closer and closer to the
3✔
154
                // sender of the payment.
3✔
155
                nextIncomingAmount lnwire.MilliSatoshi
3✔
156

3✔
157
                blindedPayment *BlindedPayment
3✔
158
        )
3✔
159

3✔
160
        pathLength := len(pathEdges)
3✔
161

3✔
162
        // When paying to a blinded route we might have appended a dummy hop at
3✔
163
        // the end to make MPP payments possible via all paths of the blinded
3✔
164
        // route set. We always append a dummy hop when the internal pathfiner
3✔
165
        // looks for a route to a blinded path which is at least one hop long
3✔
166
        // (excluding the introduction point). We add this dummy hop so that
3✔
167
        // we search for a universal target but also respect potential mc
3✔
168
        // entries which might already be present for a particular blinded path.
3✔
169
        // However when constructing the Sphinx packet we need to remove this
3✔
170
        // dummy hop again which we do here.
3✔
171
        //
3✔
172
        // NOTE: The path length is always at least 1 because there must be one
3✔
173
        // edge from the source to the destination. However we check for > 0
3✔
174
        // just for robustness here.
3✔
175
        if blindedPathSet != nil && pathLength > 0 {
6✔
176
                finalBlindedPubKey := pathEdges[pathLength-1].policy.
3✔
177
                        ToNodePubKey()
3✔
178

3✔
179
                if IsBlindedRouteNUMSTargetKey(finalBlindedPubKey[:]) {
6✔
180
                        // If the last hop is the NUMS key for blinded paths, we
3✔
181
                        // remove the dummy hop from the route.
3✔
182
                        pathEdges = pathEdges[:pathLength-1]
3✔
183
                        pathLength--
3✔
184
                }
3✔
185
        }
186

187
        for i := pathLength - 1; i >= 0; i-- {
6✔
188
                // Now we'll start to calculate the items within the per-hop
3✔
189
                // payload for the hop this edge is leading to.
3✔
190
                edge := pathEdges[i].policy
3✔
191

3✔
192
                // If this is an edge from a blinded path and the
3✔
193
                // blindedPayment variable has not been set yet, then set it now
3✔
194
                // by extracting the corresponding blinded payment from the
3✔
195
                // edge.
3✔
196
                isBlindedEdge := pathEdges[i].blindedPayment != nil
3✔
197
                if isBlindedEdge && blindedPayment == nil {
6✔
198
                        blindedPayment = pathEdges[i].blindedPayment
3✔
199
                }
3✔
200

201
                // We'll calculate the amounts, timelocks, and fees for each hop
202
                // in the route. The base case is the final hop which includes
203
                // their amount and timelocks. These values will accumulate
204
                // contributions from the preceding hops back to the sender as
205
                // we compute the route in reverse.
206
                var (
3✔
207
                        amtToForward        lnwire.MilliSatoshi
3✔
208
                        fee                 int64
3✔
209
                        totalAmtMsatBlinded lnwire.MilliSatoshi
3✔
210
                        outgoingTimeLock    uint32
3✔
211
                        customRecords       record.CustomSet
3✔
212
                        mpp                 *record.MPP
3✔
213
                        metadata            []byte
3✔
214
                )
3✔
215

3✔
216
                // Define a helper function that checks this edge's feature
3✔
217
                // vector for support for a given feature. We assume at this
3✔
218
                // point that the feature vectors transitive dependencies have
3✔
219
                // been validated.
3✔
220
                supports := func(feature lnwire.FeatureBit) bool {
6✔
221
                        // If this edge comes from router hints, the features
3✔
222
                        // could be nil.
3✔
223
                        if edge.ToNodeFeatures == nil {
3✔
224
                                return false
×
225
                        }
×
226
                        return edge.ToNodeFeatures.HasFeature(feature)
3✔
227
                }
228

229
                if i == len(pathEdges)-1 {
6✔
230
                        // If this is the last hop, then the hop payload will
3✔
231
                        // contain the exact amount. In BOLT #4: Onion Routing
3✔
232
                        // Protocol / "Payload for the Last Node", this is
3✔
233
                        // detailed.
3✔
234
                        amtToForward = finalHop.amt
3✔
235

3✔
236
                        // Fee is not part of the hop payload, but only used for
3✔
237
                        // reporting through RPC. Set to zero for the final hop.
3✔
238
                        fee = 0
3✔
239

3✔
240
                        if blindedPathSet == nil {
6✔
241
                                totalTimeLock += uint32(finalHop.cltvDelta)
3✔
242
                        } else {
6✔
243
                                totalTimeLock += uint32(
3✔
244
                                        blindedPathSet.FinalCLTVDelta(),
3✔
245
                                )
3✔
246
                        }
3✔
247
                        outgoingTimeLock = totalTimeLock
3✔
248

3✔
249
                        // Attach any custom records to the final hop.
3✔
250
                        customRecords = finalHop.records
3✔
251

3✔
252
                        // If we're attaching a payment addr but the receiver
3✔
253
                        // doesn't support both TLV and payment addrs, fail.
3✔
254
                        payAddr := supports(lnwire.PaymentAddrOptional)
3✔
255
                        if !payAddr && finalHop.paymentAddr.IsSome() {
3✔
256
                                return nil, errors.New("cannot attach " +
×
257
                                        "payment addr")
×
258
                        }
×
259

260
                        // Otherwise attach the mpp record if it exists.
261
                        // TODO(halseth): move this to payment life cycle,
262
                        // where AMP options are set.
263
                        finalHop.paymentAddr.WhenSome(func(addr [32]byte) {
6✔
264
                                mpp = record.NewMPP(finalHop.totalAmt, addr)
3✔
265
                        })
3✔
266

267
                        metadata = finalHop.metadata
3✔
268

3✔
269
                        if blindedPathSet != nil {
6✔
270
                                totalAmtMsatBlinded = finalHop.totalAmt
3✔
271
                        }
3✔
272
                } else {
3✔
273
                        // The amount that the current hop needs to forward is
3✔
274
                        // equal to the incoming amount of the next hop.
3✔
275
                        amtToForward = nextIncomingAmount
3✔
276

3✔
277
                        // The fee that needs to be paid to the current hop is
3✔
278
                        // based on the amount that this hop needs to forward
3✔
279
                        // and its policy for the outgoing channel. This policy
3✔
280
                        // is stored as part of the incoming channel of
3✔
281
                        // the next hop.
3✔
282
                        outboundFee := pathEdges[i+1].policy.ComputeFee(
3✔
283
                                amtToForward,
3✔
284
                        )
3✔
285

3✔
286
                        inboundFee := pathEdges[i].inboundFees.CalcFee(
3✔
287
                                amtToForward + outboundFee,
3✔
288
                        )
3✔
289

3✔
290
                        fee = int64(outboundFee) + inboundFee
3✔
291
                        if fee < 0 {
3✔
UNCOV
292
                                fee = 0
×
UNCOV
293
                        }
×
294

295
                        // We'll take the total timelock of the preceding hop as
296
                        // the outgoing timelock or this hop. Then we'll
297
                        // increment the total timelock incurred by this hop.
298
                        outgoingTimeLock = totalTimeLock
3✔
299
                        totalTimeLock += uint32(
3✔
300
                                pathEdges[i+1].policy.TimeLockDelta,
3✔
301
                        )
3✔
302
                }
303

304
                // Since we're traversing the path backwards atm, we prepend
305
                // each new hop such that, the final slice of hops will be in
306
                // the forwards order.
307
                currentHop := &route.Hop{
3✔
308
                        PubKeyBytes:      edge.ToNodePubKey(),
3✔
309
                        ChannelID:        edge.ChannelID,
3✔
310
                        AmtToForward:     amtToForward,
3✔
311
                        OutgoingTimeLock: outgoingTimeLock,
3✔
312
                        CustomRecords:    customRecords,
3✔
313
                        MPP:              mpp,
3✔
314
                        Metadata:         metadata,
3✔
315
                        TotalAmtMsat:     totalAmtMsatBlinded,
3✔
316
                }
3✔
317

3✔
318
                hops = append([]*route.Hop{currentHop}, hops...)
3✔
319

3✔
320
                // Finally, we update the amount that needs to flow into the
3✔
321
                // *next* hop, which is the amount this hop needs to forward,
3✔
322
                // accounting for the fee that it takes.
3✔
323
                nextIncomingAmount = amtToForward + lnwire.MilliSatoshi(fee)
3✔
324
        }
325

326
        // If we are creating a route to a blinded path, we need to add some
327
        // additional data to the route that is required for blinded forwarding.
328
        // We do another pass on our edges to append this data.
329
        if blindedPathSet != nil {
6✔
330
                // If the passed in BlindedPaymentPathSet is non-nil but no
3✔
331
                // edge had a BlindedPayment attached, it means that the path
3✔
332
                // chosen was an introduction-node-only path. So in this case,
3✔
333
                // we can assume the relevant payment is the only one in the
3✔
334
                // payment set.
3✔
335
                if blindedPayment == nil {
6✔
336
                        var err error
3✔
337
                        blindedPayment, err = blindedPathSet.IntroNodeOnlyPath()
3✔
338
                        if err != nil {
3✔
339
                                return nil, err
×
340
                        }
×
341
                }
342

343
                var (
3✔
344
                        inBlindedRoute bool
3✔
345
                        dataIndex      = 0
3✔
346

3✔
347
                        blindedPath = blindedPayment.BlindedPath
3✔
348
                        introVertex = route.NewVertex(
3✔
349
                                blindedPath.IntroductionPoint,
3✔
350
                        )
3✔
351
                )
3✔
352

3✔
353
                for i, hop := range hops {
6✔
354
                        // Once we locate our introduction node, we know that
3✔
355
                        // every hop after this is part of the blinded route.
3✔
356
                        if bytes.Equal(hop.PubKeyBytes[:], introVertex[:]) {
6✔
357
                                inBlindedRoute = true
3✔
358
                                hop.BlindingPoint = blindedPath.BlindingPoint
3✔
359
                        }
3✔
360

361
                        // We don't need to modify edges outside of our blinded
362
                        // route.
363
                        if !inBlindedRoute {
6✔
364
                                continue
3✔
365
                        }
366

367
                        payload := blindedPath.BlindedHops[dataIndex].CipherText
3✔
368
                        hop.EncryptedData = payload
3✔
369

3✔
370
                        // All of the hops in a blinded route *except* the
3✔
371
                        // final hop should have zero amounts / time locks.
3✔
372
                        if i != len(hops)-1 {
6✔
373
                                hop.AmtToForward = 0
3✔
374
                                hop.OutgoingTimeLock = 0
3✔
375
                        }
3✔
376

377
                        dataIndex++
3✔
378
                }
379
        }
380

381
        // With the base routing data expressed as hops, build the full route
382
        newRoute, err := route.NewRouteFromHops(
3✔
383
                nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex),
3✔
384
                hops,
3✔
385
        )
3✔
386
        if err != nil {
3✔
387
                return nil, err
×
388
        }
×
389

390
        return newRoute, nil
3✔
391
}
392

393
// edgeWeight computes the weight of an edge. This value is used when searching
394
// for the shortest path within the channel graph between two nodes. Weight is
395
// is the fee itself plus a time lock penalty added to it. This benefits
396
// channels with shorter time lock deltas and shorter (hops) routes in general.
397
// RiskFactor controls the influence of time lock on route selection. This is
398
// currently a fixed value, but might be configurable in the future.
399
func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi,
400
        timeLockDelta uint16) int64 {
3✔
401
        // timeLockPenalty is the penalty for the time lock delta of this channel.
3✔
402
        // It is controlled by RiskFactorBillionths and scales proportional
3✔
403
        // to the amount that will pass through channel. Rationale is that it if
3✔
404
        // a twice as large amount gets locked up, it is twice as bad.
3✔
405
        timeLockPenalty := int64(lockedAmt) * int64(timeLockDelta) *
3✔
406
                RiskFactorBillionths / 1000000000
3✔
407

3✔
408
        return int64(fee) + timeLockPenalty
3✔
409
}
3✔
410

411
// graphParams wraps the set of graph parameters passed to findPath.
412
type graphParams struct {
413
        // graph is the ChannelGraph to be used during path finding.
414
        graph Graph
415

416
        // additionalEdges is an optional set of edges that should be
417
        // considered during path finding, that is not already found in the
418
        // channel graph. These can either be private edges for bolt 11 invoices
419
        // or blinded edges when a payment to a blinded path is made.
420
        additionalEdges map[route.Vertex][]AdditionalEdge
421

422
        // bandwidthHints is an interface that provides bandwidth hints that
423
        // can provide a better estimate of the current channel bandwidth than
424
        // what is found in the graph. It will override the capacities and
425
        // disabled flags found in the graph for local channels when doing
426
        // path finding if it has updated values for that channel. In
427
        // particular, it should be set to the current available sending
428
        // bandwidth for active local channels, and 0 for inactive channels.
429
        bandwidthHints bandwidthHints
430
}
431

432
// RestrictParams wraps the set of restrictions passed to findPath that the
433
// found path must adhere to.
434
type RestrictParams struct {
435
        // ProbabilitySource is a callback that is expected to return the
436
        // success probability of traversing the channel from the node.
437
        ProbabilitySource func(route.Vertex, route.Vertex,
438
                lnwire.MilliSatoshi, btcutil.Amount) float64
439

440
        // FeeLimit is a maximum fee amount allowed to be used on the path from
441
        // the source to the target.
442
        FeeLimit lnwire.MilliSatoshi
443

444
        // OutgoingChannelIDs is the list of channels that are allowed for the
445
        // first hop. If nil, any channel may be used.
446
        OutgoingChannelIDs []uint64
447

448
        // LastHop is the pubkey of the last node before the final destination
449
        // is reached. If nil, any node may be used.
450
        LastHop *route.Vertex
451

452
        // CltvLimit is the maximum time lock of the route excluding the final
453
        // ctlv. After path finding is complete, the caller needs to increase
454
        // all cltv expiry heights with the required final cltv delta.
455
        CltvLimit uint32
456

457
        // DestCustomRecords contains the custom records to drop off at the
458
        // final hop, if any.
459
        DestCustomRecords record.CustomSet
460

461
        // DestFeatures is a feature vector describing what the final hop
462
        // supports. If none are provided, pathfinding will try to inspect any
463
        // features on the node announcement instead.
464
        DestFeatures *lnwire.FeatureVector
465

466
        // PaymentAddr is a random 32-byte value generated by the receiver to
467
        // mitigate probing vectors and payment sniping attacks on overpaid
468
        // invoices.
469
        PaymentAddr fn.Option[[32]byte]
470

471
        // Amp signals to the pathfinder that this payment is an AMP payment
472
        // and therefore it needs to account for additional AMP data in the
473
        // final hop payload size calculation.
474
        Amp *AMPOptions
475

476
        // Metadata is additional data that is sent along with the payment to
477
        // the payee.
478
        Metadata []byte
479

480
        // BlindedPaymentPathSet is necessary to determine the hop size of the
481
        // last/exit hop.
482
        BlindedPaymentPathSet *BlindedPaymentPathSet
483

484
        // FirstHopCustomRecords includes any records that should be included in
485
        // the update_add_htlc message towards our peer.
486
        FirstHopCustomRecords lnwire.CustomRecords
487
}
488

489
// PathFindingConfig defines global parameters that control the trade-off in
490
// path finding between fees and probability.
491
type PathFindingConfig struct {
492
        // AttemptCost is the fixed virtual cost in path finding of a failed
493
        // payment attempt. It is used to trade off potentially better routes
494
        // against their probability of succeeding.
495
        AttemptCost lnwire.MilliSatoshi
496

497
        // AttemptCostPPM is the proportional virtual cost in path finding of a
498
        // failed payment attempt. It is used to trade off potentially better
499
        // routes against their probability of succeeding. This parameter is
500
        // expressed in parts per million of the total payment amount.
501
        AttemptCostPPM int64
502

503
        // MinProbability defines the minimum success probability of the
504
        // returned route.
505
        MinProbability float64
506
}
507

508
// getOutgoingBalance returns the maximum available balance in any of the
509
// channels of the given node. The second return parameters is the total
510
// available balance.
511
func getOutgoingBalance(node route.Vertex, outgoingChans map[uint64]struct{},
512
        bandwidthHints bandwidthHints,
513
        g Graph) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
3✔
514

3✔
515
        var max, total lnwire.MilliSatoshi
3✔
516
        cb := func(channel *graphdb.DirectedChannel) error {
6✔
517
                if !channel.OutPolicySet {
3✔
518
                        return nil
×
519
                }
×
520

521
                chanID := channel.ChannelID
3✔
522

3✔
523
                // Enforce outgoing channel restriction.
3✔
524
                if outgoingChans != nil {
3✔
UNCOV
525
                        if _, ok := outgoingChans[chanID]; !ok {
×
UNCOV
526
                                return nil
×
UNCOV
527
                        }
×
528
                }
529

530
                bandwidth, ok := bandwidthHints.availableChanBandwidth(
3✔
531
                        chanID, 0,
3✔
532
                )
3✔
533

3✔
534
                // If the bandwidth is not available, use the channel capacity.
3✔
535
                // This can happen when a channel is added to the graph after
3✔
536
                // we've already queried the bandwidth hints.
3✔
537
                if !ok {
3✔
UNCOV
538
                        bandwidth = lnwire.NewMSatFromSatoshis(channel.Capacity)
×
UNCOV
539
                }
×
540

541
                if bandwidth > max {
6✔
542
                        max = bandwidth
3✔
543
                }
3✔
544

545
                var overflow bool
3✔
546
                total, overflow = overflowSafeAdd(total, bandwidth)
3✔
547
                if overflow {
3✔
548
                        // If the current total and the bandwidth would
×
549
                        // overflow the maximum value, we set the total to the
×
550
                        // maximum value. Which is more milli-satoshis than are
×
551
                        // in existence anyway, so the actual value is
×
552
                        // irrelevant.
×
553
                        total = lnwire.MilliSatoshi(math.MaxUint64)
×
554
                }
×
555

556
                return nil
3✔
557
        }
558

559
        // Iterate over all channels of the to node.
560
        err := g.ForEachNodeChannel(node, cb)
3✔
561
        if err != nil {
3✔
562
                return 0, 0, err
×
563
        }
×
564
        return max, total, err
3✔
565
}
566

567
// findPath attempts to find a path from the source node within the ChannelGraph
568
// to the target node that's capable of supporting a payment of `amt` value. The
569
// current approach implemented is modified version of Dijkstra's algorithm to
570
// find a single shortest path between the source node and the destination. The
571
// distance metric used for edges is related to the time-lock+fee costs along a
572
// particular edge. If a path is found, this function returns a slice of
573
// ChannelHop structs which encoded the chosen path from the target to the
574
// source. The search is performed backwards from destination node back to
575
// source. This is to properly accumulate fees that need to be paid along the
576
// path and accurately check the amount to forward at every node against the
577
// available bandwidth.
578
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
579
        self, source, target route.Vertex, amt lnwire.MilliSatoshi,
580
        timePref float64, finalHtlcExpiry int32) ([]*unifiedEdge, float64,
581
        error) {
3✔
582

3✔
583
        // Pathfinding can be a significant portion of the total payment
3✔
584
        // latency, especially on low-powered devices. Log several metrics to
3✔
585
        // aid in the analysis performance problems in this area.
3✔
586
        start := time.Now()
3✔
587
        nodesVisited := 0
3✔
588
        edgesExpanded := 0
3✔
589
        defer func() {
6✔
590
                timeElapsed := time.Since(start)
3✔
591
                log.Debugf("Pathfinding perf metrics: nodes=%v, edges=%v, "+
3✔
592
                        "time=%v", nodesVisited, edgesExpanded, timeElapsed)
3✔
593
        }()
3✔
594

595
        // If no destination features are provided, we will load what features
596
        // we have for the target node from our graph.
597
        features := r.DestFeatures
3✔
598
        if features == nil {
6✔
599
                var err error
3✔
600
                features, err = g.graph.FetchNodeFeatures(target)
3✔
601
                if err != nil {
3✔
602
                        return nil, 0, err
×
603
                }
×
604
        }
605

606
        // Ensure that the destination's features don't include unknown
607
        // required features.
608
        err := feature.ValidateRequired(features)
3✔
609
        if err != nil {
3✔
UNCOV
610
                log.Warnf("Pathfinding destination node features: %v", err)
×
UNCOV
611
                return nil, 0, errUnknownRequiredFeature
×
UNCOV
612
        }
×
613

614
        // Ensure that all transitive dependencies are set.
615
        err = feature.ValidateDeps(features)
3✔
616
        if err != nil {
3✔
UNCOV
617
                log.Warnf("Pathfinding destination node features: %v", err)
×
UNCOV
618
                return nil, 0, errMissingDependentFeature
×
UNCOV
619
        }
×
620

621
        // Now that we know the feature vector is well-formed, we'll proceed in
622
        // checking that it supports the features we need. If the caller has a
623
        // payment address to attach, check that our destination feature vector
624
        // supports them.
625
        if r.PaymentAddr.IsSome() &&
3✔
626
                !features.HasFeature(lnwire.PaymentAddrOptional) {
3✔
UNCOV
627

×
UNCOV
628
                return nil, 0, errNoPaymentAddr
×
UNCOV
629
        }
×
630

631
        // Set up outgoing channel map for quicker access.
632
        var outgoingChanMap map[uint64]struct{}
3✔
633
        if len(r.OutgoingChannelIDs) > 0 {
3✔
UNCOV
634
                outgoingChanMap = make(map[uint64]struct{})
×
UNCOV
635
                for _, outChan := range r.OutgoingChannelIDs {
×
UNCOV
636
                        outgoingChanMap[outChan] = struct{}{}
×
UNCOV
637
                }
×
638
        }
639

640
        // If we are routing from ourselves, check that we have enough local
641
        // balance available.
642
        if source == self {
6✔
643
                max, total, err := getOutgoingBalance(
3✔
644
                        self, outgoingChanMap, g.bandwidthHints, g.graph,
3✔
645
                )
3✔
646
                if err != nil {
3✔
647
                        return nil, 0, err
×
648
                }
×
649

650
                // If the total outgoing balance isn't sufficient, it will be
651
                // impossible to complete the payment.
652
                if total < amt {
6✔
653
                        log.Warnf("Not enough outbound balance to send "+
3✔
654
                                "htlc of amount: %v, only have local "+
3✔
655
                                "balance: %v", amt, total)
3✔
656

3✔
657
                        return nil, 0, errInsufficientBalance
3✔
658
                }
3✔
659

660
                // If there is only not enough capacity on a single route, it
661
                // may still be possible to complete the payment by splitting.
662
                if max < amt {
6✔
663
                        return nil, 0, errNoPathFound
3✔
664
                }
3✔
665
        }
666

667
        // First we'll initialize an empty heap which'll help us to quickly
668
        // locate the next edge we should visit next during our graph
669
        // traversal.
670
        nodeHeap := newDistanceHeap(estimatedNodeCount)
3✔
671

3✔
672
        // Holds the current best distance for a given node.
3✔
673
        distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount)
3✔
674

3✔
675
        additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
3✔
676
        for vertex, additionalEdges := range g.additionalEdges {
6✔
677
                // Edges connected to self are always included in the graph,
3✔
678
                // therefore can be skipped. This prevents us from trying
3✔
679
                // routes to malformed hop hints.
3✔
680
                if vertex == self {
6✔
681
                        continue
3✔
682
                }
683

684
                // Build reverse lookup to find incoming edges. Needed because
685
                // search is taken place from target to source.
686
                for _, additionalEdge := range additionalEdges {
6✔
687
                        outgoingEdgePolicy := additionalEdge.EdgePolicy()
3✔
688
                        toVertex := outgoingEdgePolicy.ToNodePubKey()
3✔
689

3✔
690
                        incomingEdgePolicy := &edgePolicyWithSource{
3✔
691
                                sourceNode: vertex,
3✔
692
                                edge:       additionalEdge,
3✔
693
                        }
3✔
694

3✔
695
                        additionalEdgesWithSrc[toVertex] =
3✔
696
                                append(additionalEdgesWithSrc[toVertex],
3✔
697
                                        incomingEdgePolicy)
3✔
698
                }
3✔
699
        }
700

701
        // The payload size of the final hop differ from intermediate hops
702
        // and depends on whether the destination is blinded or not.
703
        lastHopPayloadSize, err := lastHopPayloadSize(r, finalHtlcExpiry, amt)
3✔
704
        if err != nil {
3✔
705
                return nil, 0, err
×
706
        }
×
707

708
        // We can't always assume that the end destination is publicly
709
        // advertised to the network so we'll manually include the target node.
710
        // The target node charges no fee. Distance is set to 0, because this is
711
        // the starting point of the graph traversal. We are searching backwards
712
        // to get the fees first time right and correctly match channel
713
        // bandwidth.
714
        //
715
        // Don't record the initial partial path in the distance map and reserve
716
        // that key for the source key in the case we route to ourselves.
717
        partialPath := &nodeWithDist{
3✔
718
                dist:              0,
3✔
719
                weight:            0,
3✔
720
                node:              target,
3✔
721
                netAmountReceived: amt,
3✔
722
                incomingCltv:      finalHtlcExpiry,
3✔
723
                probability:       1,
3✔
724
                routingInfoSize:   lastHopPayloadSize,
3✔
725
        }
3✔
726

3✔
727
        // Calculate the absolute cltv limit. Use uint64 to prevent an overflow
3✔
728
        // if the cltv limit is MaxUint32.
3✔
729
        absoluteCltvLimit := uint64(r.CltvLimit) + uint64(finalHtlcExpiry)
3✔
730

3✔
731
        // Calculate the default attempt cost as configured globally.
3✔
732
        defaultAttemptCost := float64(
3✔
733
                cfg.AttemptCost +
3✔
734
                        amt*lnwire.MilliSatoshi(cfg.AttemptCostPPM)/1000000,
3✔
735
        )
3✔
736

3✔
737
        // Validate time preference value.
3✔
738
        if math.Abs(timePref) > 1 {
3✔
739
                return nil, 0, fmt.Errorf("time preference %v out of range "+
×
740
                        "[-1, 1]", timePref)
×
741
        }
×
742

743
        // Scale to avoid the extremes -1 and 1 which run into infinity issues.
744
        timePref *= 0.9
3✔
745

3✔
746
        // Apply time preference. At 0, the default attempt cost will
3✔
747
        // be used.
3✔
748
        absoluteAttemptCost := defaultAttemptCost * (1/(0.5-timePref/2) - 1)
3✔
749

3✔
750
        log.Debugf("Pathfinding absolute attempt cost: %v sats",
3✔
751
                absoluteAttemptCost/1000)
3✔
752

3✔
753
        // processEdge is a helper closure that will be used to make sure edges
3✔
754
        // satisfy our specific requirements.
3✔
755
        processEdge := func(fromVertex route.Vertex,
3✔
756
                edge *unifiedEdge, toNodeDist *nodeWithDist) {
6✔
757

3✔
758
                edgesExpanded++
3✔
759

3✔
760
                // Calculate inbound fee charged by "to" node. The exit hop
3✔
761
                // doesn't charge inbound fees. If the "to" node is the exit
3✔
762
                // hop, its inbound fees have already been set to zero by
3✔
763
                // nodeEdgeUnifier.
3✔
764
                inboundFee := edge.inboundFees.CalcFee(
3✔
765
                        toNodeDist.netAmountReceived,
3✔
766
                )
3✔
767

3✔
768
                // Make sure that the node total fee is never negative.
3✔
769
                // Routing nodes treat a total fee that turns out
3✔
770
                // negative as a zero fee and pathfinding should do the
3✔
771
                // same.
3✔
772
                minInboundFee := -int64(toNodeDist.outboundFee)
3✔
773
                if inboundFee < minInboundFee {
3✔
UNCOV
774
                        inboundFee = minInboundFee
×
UNCOV
775
                }
×
776

777
                // Calculate amount that the candidate node would have to send
778
                // out.
779
                amountToSend := toNodeDist.netAmountReceived +
3✔
780
                        lnwire.MilliSatoshi(inboundFee)
3✔
781

3✔
782
                // Check if accumulated fees would exceed fee limit when this
3✔
783
                // node would be added to the path.
3✔
784
                totalFee := int64(amountToSend) - int64(amt)
3✔
785

3✔
786
                log.Trace(lnutils.NewLogClosure(func() string {
3✔
787
                        return fmt.Sprintf(
×
788
                                "Checking fromVertex (%v) with "+
×
789
                                        "minInboundFee=%v, inboundFee=%v, "+
×
790
                                        "amountToSend=%v, amt=%v, totalFee=%v",
×
791
                                fromVertex, minInboundFee, inboundFee,
×
792
                                amountToSend, amt, totalFee,
×
793
                        )
×
794
                }))
×
795

796
                if totalFee > 0 && lnwire.MilliSatoshi(totalFee) > r.FeeLimit {
3✔
UNCOV
797
                        return
×
UNCOV
798
                }
×
799

800
                // Request the success probability for this edge.
801
                edgeProbability := r.ProbabilitySource(
3✔
802
                        fromVertex, toNodeDist.node, amountToSend,
3✔
803
                        edge.capacity,
3✔
804
                )
3✔
805

3✔
806
                log.Trace(lnutils.NewLogClosure(func() string {
3✔
807
                        return fmt.Sprintf("path finding probability: fromnode=%v,"+
×
808
                                " tonode=%v, amt=%v, cap=%v, probability=%v",
×
809
                                fromVertex, toNodeDist.node, amountToSend,
×
810
                                edge.capacity, edgeProbability)
×
811
                }))
×
812

813
                // If the probability is zero, there is no point in trying.
814
                if edgeProbability == 0 {
3✔
815
                        return
×
816
                }
×
817

818
                // Compute fee that fromVertex is charging. It is based on the
819
                // amount that needs to be sent to the next node in the route.
820
                //
821
                // Source node has no predecessor to pay a fee. Therefore set
822
                // fee to zero, because it should not be included in the fee
823
                // limit check and edge weight.
824
                //
825
                // Also determine the time lock delta that will be added to the
826
                // route if fromVertex is selected. If fromVertex is the source
827
                // node, no additional timelock is required.
828
                var (
3✔
829
                        timeLockDelta uint16
3✔
830
                        outboundFee   int64
3✔
831
                )
3✔
832

3✔
833
                if fromVertex != source {
6✔
834
                        outboundFee = int64(
3✔
835
                                edge.policy.ComputeFee(amountToSend),
3✔
836
                        )
3✔
837
                        timeLockDelta = edge.policy.TimeLockDelta
3✔
838
                }
3✔
839

840
                incomingCltv := toNodeDist.incomingCltv + int32(timeLockDelta)
3✔
841

3✔
842
                // Check that we are within our CLTV limit.
3✔
843
                if uint64(incomingCltv) > absoluteCltvLimit {
3✔
UNCOV
844
                        return
×
UNCOV
845
                }
×
846

847
                // netAmountToReceive is the amount that the node that is added
848
                // to the distance map needs to receive from a (to be found)
849
                // previous node in the route. The inbound fee of the receiving
850
                // node is already subtracted from this value. The previous node
851
                // will need to pay the amount that this node forwards plus the
852
                // fee it charges plus this node's inbound fee.
853
                netAmountToReceive := amountToSend +
3✔
854
                        lnwire.MilliSatoshi(outboundFee)
3✔
855

3✔
856
                // Calculate total probability of successfully reaching target
3✔
857
                // by multiplying the probabilities. Both this edge and the rest
3✔
858
                // of the route must succeed.
3✔
859
                probability := toNodeDist.probability * edgeProbability
3✔
860

3✔
861
                // If the probability is below the specified lower bound, we can
3✔
862
                // abandon this direction. Adding further nodes can only lower
3✔
863
                // the probability more.
3✔
864
                if probability < cfg.MinProbability {
6✔
865
                        return
3✔
866
                }
3✔
867

868
                // Calculate the combined fee for this edge. Dijkstra does not
869
                // support negative edge weights. Because this fee feeds into
870
                // the edge weight calculation, we don't allow it to be
871
                // negative.
872
                signedFee := inboundFee + outboundFee
3✔
873
                fee := lnwire.MilliSatoshi(0)
3✔
874
                if signedFee > 0 {
6✔
875
                        fee = lnwire.MilliSatoshi(signedFee)
3✔
876
                }
3✔
877

878
                // By adding fromVertex in the route, there will be an extra
879
                // weight composed of the fee that this node will charge and
880
                // the amount that will be locked for timeLockDelta blocks in
881
                // the HTLC that is handed out to fromVertex.
882
                weight := edgeWeight(amountToSend, fee, timeLockDelta)
3✔
883

3✔
884
                // Compute the tentative weight to this new channel/edge
3✔
885
                // which is the weight from our toNode to the target node
3✔
886
                // plus the weight of this edge.
3✔
887
                tempWeight := toNodeDist.weight + weight
3✔
888

3✔
889
                // Add an extra factor to the weight to take into account the
3✔
890
                // probability. Another reason why we rounded the fee up to zero
3✔
891
                // is to prevent a highly negative fee from cancelling out the
3✔
892
                // extra factor. We don't want an always-failing node to attract
3✔
893
                // traffic using a highly negative fee and escape penalization.
3✔
894
                tempDist := getProbabilityBasedDist(
3✔
895
                        tempWeight, probability,
3✔
896
                        absoluteAttemptCost,
3✔
897
                )
3✔
898

3✔
899
                // If there is already a best route stored, compare this
3✔
900
                // candidate route with the best route so far.
3✔
901
                current, ok := distance[fromVertex]
3✔
902
                if ok {
6✔
903
                        // If this route is worse than what we already found,
3✔
904
                        // skip this route.
3✔
905
                        if tempDist > current.dist {
6✔
906
                                return
3✔
907
                        }
3✔
908

909
                        // If the route is equally good and the probability
910
                        // isn't better, skip this route. It is important to
911
                        // also return if both cost and probability are equal,
912
                        // because otherwise the algorithm could run into an
913
                        // endless loop.
914
                        probNotBetter := probability <= current.probability
3✔
915
                        if tempDist == current.dist && probNotBetter {
6✔
916
                                return
3✔
917
                        }
3✔
918
                }
919

920
                // Calculate the total routing info size if this hop were to be
921
                // included. If we are coming from the source hop, the payload
922
                // size is zero, because the original htlc isn't in the onion
923
                // blob.
924
                //
925
                // NOTE: For blinded paths with the NUMS key as the last hop,
926
                // the payload size accounts for this dummy hop which is of
927
                // the same size as the real last hop. So we account for a
928
                // bigger size than the route is however we accept this
929
                // little inaccuracy here because we are over estimating by
930
                // 1 hop.
931
                var payloadSize uint64
3✔
932
                if fromVertex != source {
6✔
933
                        // In case the unifiedEdge does not have a payload size
3✔
934
                        // function supplied we request a graceful shutdown
3✔
935
                        // because this should never happen.
3✔
936
                        if edge.hopPayloadSizeFn == nil {
3✔
937
                                log.Criticalf("No payload size function "+
×
938
                                        "available for edge=%v unable to "+
×
939
                                        "determine payload size: %v", edge,
×
940
                                        ErrNoPayLoadSizeFunc)
×
941

×
942
                                return
×
943
                        }
×
944

945
                        payloadSize = edge.hopPayloadSizeFn(
3✔
946
                                amountToSend,
3✔
947
                                uint32(toNodeDist.incomingCltv),
3✔
948
                                edge.policy.ChannelID,
3✔
949
                        )
3✔
950
                }
951

952
                routingInfoSize := toNodeDist.routingInfoSize + payloadSize
3✔
953
                // Skip paths that would exceed the maximum routing info size.
3✔
954
                if routingInfoSize > sphinx.MaxPayloadSize {
3✔
UNCOV
955
                        return
×
UNCOV
956
                }
×
957

958
                // All conditions are met and this new tentative distance is
959
                // better than the current best known distance to this node.
960
                // The new better distance is recorded, and also our "next hop"
961
                // map is populated with this edge.
962
                withDist := &nodeWithDist{
3✔
963
                        dist:              tempDist,
3✔
964
                        weight:            tempWeight,
3✔
965
                        node:              fromVertex,
3✔
966
                        netAmountReceived: netAmountToReceive,
3✔
967
                        outboundFee:       lnwire.MilliSatoshi(outboundFee),
3✔
968
                        incomingCltv:      incomingCltv,
3✔
969
                        probability:       probability,
3✔
970
                        nextHop:           edge,
3✔
971
                        routingInfoSize:   routingInfoSize,
3✔
972
                }
3✔
973
                distance[fromVertex] = withDist
3✔
974

3✔
975
                // Either push withDist onto the heap if the node
3✔
976
                // represented by fromVertex is not already on the heap OR adjust
3✔
977
                // its position within the heap via heap.Fix.
3✔
978
                nodeHeap.PushOrFix(withDist)
3✔
979
        }
980

981
        // TODO(roasbeef): also add path caching
982
        //  * similar to route caching, but doesn't factor in the amount
983

984
        // Cache features because we visit nodes multiple times.
985
        featureCache := make(map[route.Vertex]*lnwire.FeatureVector)
3✔
986

3✔
987
        // getGraphFeatures returns (cached) node features from the graph.
3✔
988
        getGraphFeatures := func(node route.Vertex) (*lnwire.FeatureVector,
3✔
989
                error) {
6✔
990

3✔
991
                // Check cache for features of the fromNode.
3✔
992
                fromFeatures, ok := featureCache[node]
3✔
993
                if ok {
6✔
994
                        return fromFeatures, nil
3✔
995
                }
3✔
996

997
                // Fetch node features fresh from the graph.
998
                fromFeatures, err := g.graph.FetchNodeFeatures(node)
3✔
999
                if err != nil {
3✔
1000
                        return nil, err
×
1001
                }
×
1002

1003
                // Don't route through nodes that contain unknown required
1004
                // features and mark as nil in the cache.
1005
                err = feature.ValidateRequired(fromFeatures)
3✔
1006
                if err != nil {
3✔
UNCOV
1007
                        featureCache[node] = nil
×
UNCOV
1008
                        return nil, nil
×
UNCOV
1009
                }
×
1010

1011
                // Don't route through nodes that don't properly set all
1012
                // transitive feature dependencies and mark as nil in the cache.
1013
                err = feature.ValidateDeps(fromFeatures)
3✔
1014
                if err != nil {
3✔
UNCOV
1015
                        featureCache[node] = nil
×
UNCOV
1016
                        return nil, nil
×
UNCOV
1017
                }
×
1018

1019
                // Update cache.
1020
                featureCache[node] = fromFeatures
3✔
1021

3✔
1022
                return fromFeatures, nil
3✔
1023
        }
1024

1025
        routeToSelf := source == target
3✔
1026
        for {
6✔
1027
                nodesVisited++
3✔
1028

3✔
1029
                pivot := partialPath.node
3✔
1030
                isExitHop := partialPath.nextHop == nil
3✔
1031

3✔
1032
                // Create unified policies for all incoming connections. Don't
3✔
1033
                // use inbound fees for the exit hop.
3✔
1034
                u := newNodeEdgeUnifier(
3✔
1035
                        self, pivot, !isExitHop, outgoingChanMap,
3✔
1036
                )
3✔
1037

3✔
1038
                err := u.addGraphPolicies(g.graph)
3✔
1039
                if err != nil {
3✔
1040
                        return nil, 0, err
×
1041
                }
×
1042

1043
                // We add hop hints that were supplied externally.
1044
                for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
6✔
1045
                        // Assume zero inbound fees for route hints. If inbound
3✔
1046
                        // fees would apply, they couldn't be communicated in
3✔
1047
                        // bolt11 invoices currently.
3✔
1048
                        inboundFee := models.InboundFee{}
3✔
1049

3✔
1050
                        // Hop hints don't contain a capacity. We set one here,
3✔
1051
                        // since a capacity is needed for probability
3✔
1052
                        // calculations. We set a high capacity to act as if
3✔
1053
                        // there is enough liquidity, otherwise the hint would
3✔
1054
                        // not have been added by a wallet.
3✔
1055
                        // We also pass the payload size function to the
3✔
1056
                        // graph data so that we calculate the exact payload
3✔
1057
                        // size when evaluating this hop for a route.
3✔
1058
                        u.addPolicy(
3✔
1059
                                reverseEdge.sourceNode,
3✔
1060
                                reverseEdge.edge.EdgePolicy(),
3✔
1061
                                inboundFee,
3✔
1062
                                fakeHopHintCapacity,
3✔
1063
                                reverseEdge.edge.IntermediatePayloadSize,
3✔
1064
                                reverseEdge.edge.BlindedPayment(),
3✔
1065
                        )
3✔
1066
                }
3✔
1067

1068
                netAmountReceived := partialPath.netAmountReceived
3✔
1069

3✔
1070
                // Expand all connections using the optimal policy for each
3✔
1071
                // connection.
3✔
1072
                for fromNode, edgeUnifier := range u.edgeUnifiers {
6✔
1073
                        // The target node is not recorded in the distance map.
3✔
1074
                        // Therefore we need to have this check to prevent
3✔
1075
                        // creating a cycle. Only when we intend to route to
3✔
1076
                        // self, we allow this cycle to form. In that case we'll
3✔
1077
                        // also break out of the search loop below.
3✔
1078
                        if !routeToSelf && fromNode == target {
6✔
1079
                                continue
3✔
1080
                        }
1081

1082
                        // Apply last hop restriction if set.
1083
                        if r.LastHop != nil &&
3✔
1084
                                pivot == target && fromNode != *r.LastHop {
3✔
UNCOV
1085

×
UNCOV
1086
                                continue
×
1087
                        }
1088

1089
                        edge := edgeUnifier.getEdge(
3✔
1090
                                netAmountReceived, g.bandwidthHints,
3✔
1091
                                partialPath.outboundFee,
3✔
1092
                        )
3✔
1093

3✔
1094
                        if edge == nil {
6✔
1095
                                continue
3✔
1096
                        }
1097

1098
                        // Get feature vector for fromNode.
1099
                        fromFeatures, err := getGraphFeatures(fromNode)
3✔
1100
                        if err != nil {
3✔
1101
                                return nil, 0, err
×
1102
                        }
×
1103

1104
                        // If there are no valid features, skip this node.
1105
                        if fromFeatures == nil {
3✔
UNCOV
1106
                                continue
×
1107
                        }
1108

1109
                        // Check if this candidate node is better than what we
1110
                        // already have.
1111
                        processEdge(fromNode, edge, partialPath)
3✔
1112
                }
1113

1114
                if nodeHeap.Len() == 0 {
6✔
1115
                        break
3✔
1116
                }
1117

1118
                // Fetch the node within the smallest distance from our source
1119
                // from the heap.
1120
                partialPath = heap.Pop(&nodeHeap).(*nodeWithDist)
3✔
1121

3✔
1122
                // If we've reached our source (or we don't have any incoming
3✔
1123
                // edges), then we're done here and can exit the graph
3✔
1124
                // traversal early.
3✔
1125
                if partialPath.node == source {
6✔
1126
                        break
3✔
1127
                }
1128
        }
1129

1130
        // Use the distance map to unravel the forward path from source to
1131
        // target.
1132
        var pathEdges []*unifiedEdge
3✔
1133
        currentNode := source
3✔
1134
        for {
6✔
1135
                // Determine the next hop forward using the next map.
3✔
1136
                currentNodeWithDist, ok := distance[currentNode]
3✔
1137
                if !ok {
6✔
1138
                        // If the node doesn't have a next hop it means we
3✔
1139
                        // didn't find a path.
3✔
1140
                        return nil, 0, errNoPathFound
3✔
1141
                }
3✔
1142

1143
                // Add the next hop to the list of path edges.
1144
                pathEdges = append(pathEdges, currentNodeWithDist.nextHop)
3✔
1145

3✔
1146
                // Advance current node.
3✔
1147
                currentNode = currentNodeWithDist.nextHop.policy.ToNodePubKey()
3✔
1148

3✔
1149
                // Check stop condition at the end of this loop. This prevents
3✔
1150
                // breaking out too soon for self-payments that have target set
3✔
1151
                // to source.
3✔
1152
                if currentNode == target {
6✔
1153
                        break
3✔
1154
                }
1155
        }
1156

1157
        // For the final hop, we'll set the node features to those determined
1158
        // above. These are either taken from the destination features, e.g.
1159
        // virtual or invoice features, or loaded as a fallback from the graph.
1160
        // The transitive dependencies were already validated above, so no need
1161
        // to do so now.
1162
        //
1163
        // NOTE: This may overwrite features loaded from the graph if
1164
        // destination features were provided. This is fine though, since our
1165
        // route construction does not care where the features are actually
1166
        // taken from. In the future we may wish to do route construction within
1167
        // findPath, and avoid using ChannelEdgePolicy altogether.
1168
        pathEdges[len(pathEdges)-1].policy.ToNodeFeatures = features
3✔
1169

3✔
1170
        log.Debugf("Found route: probability=%v, hops=%v, fee=%v",
3✔
1171
                distance[source].probability, len(pathEdges),
3✔
1172
                distance[source].netAmountReceived-amt)
3✔
1173

3✔
1174
        return pathEdges, distance[source].probability, nil
3✔
1175
}
1176

1177
// blindedPathRestrictions are a set of constraints to adhere to when
1178
// choosing a set of blinded paths to this node.
1179
type blindedPathRestrictions struct {
1180
        // minNumHops is the minimum number of hops to include in a blinded
1181
        // path. This doesn't include our node, so if the minimum is 1, then
1182
        // the path will contain at minimum our node along with an introduction
1183
        // node hop. A minimum of 0 will include paths where this node is the
1184
        // introduction node and so should be used with caution.
1185
        minNumHops uint8
1186

1187
        // maxNumHops is the maximum number of hops to include in a blinded
1188
        // path. This doesn't include our node, so if the maximum is 1, then
1189
        // the path will contain our node along with an introduction node hop.
1190
        maxNumHops uint8
1191

1192
        // nodeOmissionSet holds a set of node IDs of nodes that we should
1193
        // ignore during blinded path selection.
1194
        nodeOmissionSet fn.Set[route.Vertex]
1195
}
1196

1197
// blindedHop holds the information about a hop we have selected for a blinded
1198
// path.
1199
type blindedHop struct {
1200
        vertex       route.Vertex
1201
        channelID    uint64
1202
        edgeCapacity btcutil.Amount
1203
}
1204

1205
// findBlindedPaths does a depth first search from the target node to find a set
1206
// of blinded paths to the target node given the set of restrictions. This
1207
// function will select and return any candidate path. A candidate path is a
1208
// path to the target node with a size determined by the given hop number
1209
// constraints where all the nodes on the path signal the route blinding feature
1210
// _and_ the introduction node for the path has more than one public channel.
1211
// Any filtering of paths based on payment value or success probabilities is
1212
// left to the caller.
1213
func findBlindedPaths(g Graph, target route.Vertex,
1214
        restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
3✔
1215

3✔
1216
        // Sanity check the restrictions.
3✔
1217
        if restrictions.minNumHops > restrictions.maxNumHops {
3✔
1218
                return nil, fmt.Errorf("maximum number of blinded path hops "+
×
1219
                        "(%d) must be greater than or equal to the minimum "+
×
1220
                        "number of hops (%d)", restrictions.maxNumHops,
×
1221
                        restrictions.minNumHops)
×
1222
        }
×
1223

1224
        // If the node is not the destination node, then it is required that the
1225
        // node advertise the route blinding feature-bit in order for it to be
1226
        // chosen as a node on the blinded path.
1227
        supportsRouteBlinding := func(node route.Vertex) (bool, error) {
6✔
1228
                if node == target {
6✔
1229
                        return true, nil
3✔
1230
                }
3✔
1231

1232
                features, err := g.FetchNodeFeatures(node)
3✔
1233
                if err != nil {
3✔
1234
                        return false, err
×
1235
                }
×
1236

1237
                return features.HasFeature(lnwire.RouteBlindingOptional), nil
3✔
1238
        }
1239

1240
        // This function will have some recursion. We will spin out from the
1241
        // target node & append edges to the paths until we reach various exit
1242
        // conditions such as: The maxHops number being reached or reaching
1243
        // a node that doesn't have any other edges - in that final case, the
1244
        // whole path should be ignored.
1245
        paths, _, err := processNodeForBlindedPath(
3✔
1246
                g, target, supportsRouteBlinding, nil, restrictions,
3✔
1247
        )
3✔
1248
        if err != nil {
3✔
1249
                return nil, err
×
1250
        }
×
1251

1252
        // Reverse each path so that the order is correct (from introduction
1253
        // node to last hop node) and then append this node on as the
1254
        // destination of each path.
1255
        orderedPaths := make([][]blindedHop, len(paths))
3✔
1256
        for i, path := range paths {
6✔
1257
                sort.Slice(path, func(i, j int) bool {
6✔
1258
                        return j < i
3✔
1259
                })
3✔
1260

1261
                orderedPaths[i] = append(path, blindedHop{vertex: target})
3✔
1262
        }
1263

1264
        // Handle the special case that allows a blinded path with the
1265
        // introduction node as the destination node.
1266
        if restrictions.minNumHops == 0 {
6✔
1267
                singleHopPath := [][]blindedHop{{{vertex: target}}}
3✔
1268

3✔
1269
                //nolint:makezero
3✔
1270
                orderedPaths = append(
3✔
1271
                        orderedPaths, singleHopPath...,
3✔
1272
                )
3✔
1273
        }
3✔
1274

1275
        return orderedPaths, err
3✔
1276
}
1277

1278
// processNodeForBlindedPath is a recursive function that traverses the graph
1279
// in a depth first manner searching for a set of blinded paths to the given
1280
// node.
1281
func processNodeForBlindedPath(g Graph, node route.Vertex,
1282
        supportsRouteBlinding func(vertex route.Vertex) (bool, error),
1283
        alreadyVisited map[route.Vertex]bool,
1284
        restrictions *blindedPathRestrictions) ([][]blindedHop, bool, error) {
3✔
1285

3✔
1286
        // If we have already visited the maximum number of hops, then this path
3✔
1287
        // is complete and we can exit now.
3✔
1288
        if len(alreadyVisited) > int(restrictions.maxNumHops) {
6✔
1289
                return nil, false, nil
3✔
1290
        }
3✔
1291

1292
        // If we have already visited this peer on this path, then we skip
1293
        // processing it again.
1294
        if alreadyVisited[node] {
6✔
1295
                return nil, false, nil
3✔
1296
        }
3✔
1297

1298
        // If we have explicitly been told to ignore this node for blinded paths
1299
        // then we skip it too.
1300
        if restrictions.nodeOmissionSet.Contains(node) {
3✔
UNCOV
1301
                return nil, false, nil
×
UNCOV
1302
        }
×
1303

1304
        supports, err := supportsRouteBlinding(node)
3✔
1305
        if err != nil {
3✔
1306
                return nil, false, err
×
1307
        }
×
1308
        if !supports {
6✔
1309
                return nil, false, nil
3✔
1310
        }
3✔
1311

1312
        // At this point, copy the alreadyVisited map.
1313
        visited := make(map[route.Vertex]bool, len(alreadyVisited))
3✔
1314
        for r := range alreadyVisited {
6✔
1315
                visited[r] = true
3✔
1316
        }
3✔
1317

1318
        // Add this node the visited set.
1319
        visited[node] = true
3✔
1320

3✔
1321
        var (
3✔
1322
                hopSets   [][]blindedHop
3✔
1323
                chanCount int
3✔
1324
        )
3✔
1325

3✔
1326
        // Now, iterate over the node's channels in search for paths to this
3✔
1327
        // node that can be used for blinded paths
3✔
1328
        err = g.ForEachNodeChannel(node,
3✔
1329
                func(channel *graphdb.DirectedChannel) error {
6✔
1330
                        // Keep track of how many incoming channels this node
3✔
1331
                        // has. We only use a node as an introduction node if it
3✔
1332
                        // has channels other than the one that lead us to it.
3✔
1333
                        chanCount++
3✔
1334

3✔
1335
                        // Process each channel peer to gather any paths that
3✔
1336
                        // lead to the peer.
3✔
1337
                        nextPaths, hasMoreChans, err := processNodeForBlindedPath( //nolint:ll
3✔
1338
                                g, channel.OtherNode, supportsRouteBlinding,
3✔
1339
                                visited, restrictions,
3✔
1340
                        )
3✔
1341
                        if err != nil {
3✔
1342
                                return err
×
1343
                        }
×
1344

1345
                        hop := blindedHop{
3✔
1346
                                vertex:       channel.OtherNode,
3✔
1347
                                channelID:    channel.ChannelID,
3✔
1348
                                edgeCapacity: channel.Capacity,
3✔
1349
                        }
3✔
1350

3✔
1351
                        // For each of the paths returned, unwrap them and
3✔
1352
                        // append this hop to them.
3✔
1353
                        for _, path := range nextPaths {
6✔
1354
                                hopSets = append(
3✔
1355
                                        hopSets,
3✔
1356
                                        append([]blindedHop{hop}, path...),
3✔
1357
                                )
3✔
1358
                        }
3✔
1359

1360
                        // If this node does have channels other than the one
1361
                        // that lead to it, and if the hop count up to this node
1362
                        // meets the minHop requirement, then we also add a
1363
                        // path that starts at this node.
1364
                        if hasMoreChans &&
3✔
1365
                                len(visited) >= int(restrictions.minNumHops) {
6✔
1366

3✔
1367
                                hopSets = append(hopSets, []blindedHop{hop})
3✔
1368
                        }
3✔
1369

1370
                        return nil
3✔
1371
                },
1372
        )
1373
        if err != nil {
3✔
1374
                return nil, false, err
×
1375
        }
×
1376

1377
        return hopSets, chanCount > 1, nil
3✔
1378
}
1379

1380
// getProbabilityBasedDist converts a weight into a distance that takes into
1381
// account the success probability and the (virtual) cost of a failed payment
1382
// attempt.
1383
//
1384
// Derivation:
1385
//
1386
// Suppose there are two routes A and B with fees Fa and Fb and success
1387
// probabilities Pa and Pb.
1388
//
1389
// Is the expected cost of trying route A first and then B lower than trying the
1390
// other way around?
1391
//
1392
// The expected cost of A-then-B is: Pa*Fa + (1-Pa)*Pb*(c+Fb)
1393
//
1394
// The expected cost of B-then-A is: Pb*Fb + (1-Pb)*Pa*(c+Fa)
1395
//
1396
// In these equations, the term representing the case where both A and B fail is
1397
// left out because its value would be the same in both cases.
1398
//
1399
// Pa*Fa + (1-Pa)*Pb*(c+Fb) < Pb*Fb + (1-Pb)*Pa*(c+Fa)
1400
//
1401
// Pa*Fa + Pb*c + Pb*Fb - Pa*Pb*c - Pa*Pb*Fb < Pb*Fb + Pa*c + Pa*Fa - Pa*Pb*c - Pa*Pb*Fa
1402
//
1403
// Removing terms that cancel out:
1404
// Pb*c - Pa*Pb*Fb < Pa*c - Pa*Pb*Fa
1405
//
1406
// Divide by Pa*Pb:
1407
// c/Pa - Fb < c/Pb - Fa
1408
//
1409
// Move terms around:
1410
// Fa + c/Pa < Fb + c/Pb
1411
//
1412
// So the value of F + c/P can be used to compare routes.
1413
func getProbabilityBasedDist(weight int64, probability float64,
1414
        penalty float64) int64 {
3✔
1415

3✔
1416
        // Prevent divide by zero by returning early.
3✔
1417
        if probability == 0 {
3✔
1418
                return infinity
×
1419
        }
×
1420

1421
        // Calculate distance.
1422
        dist := float64(weight) + penalty/probability
3✔
1423

3✔
1424
        // Avoid cast if an overflow would occur. The maxFloat constant is
3✔
1425
        // chosen to stay well below the maximum float64 value that is still
3✔
1426
        // convertible to int64.
3✔
1427
        const maxFloat = 9000000000000000000
3✔
1428
        if dist > maxFloat {
3✔
1429
                return infinity
×
1430
        }
×
1431

1432
        return int64(dist)
3✔
1433
}
1434

1435
// lastHopPayloadSize calculates the payload size of the final hop in a route.
1436
// It depends on the tlv types which are present and also whether the hop is
1437
// part of a blinded route or not.
1438
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
1439
        amount lnwire.MilliSatoshi) (uint64, error) {
3✔
1440

3✔
1441
        if r.BlindedPaymentPathSet != nil {
6✔
1442
                paymentPath, err := r.BlindedPaymentPathSet.
3✔
1443
                        LargestLastHopPayloadPath()
3✔
1444
                if err != nil {
3✔
1445
                        return 0, err
×
1446
                }
×
1447

1448
                blindedPath := paymentPath.BlindedPath.BlindedHops
3✔
1449
                blindedPoint := paymentPath.BlindedPath.BlindingPoint
3✔
1450

3✔
1451
                encryptedData := blindedPath[len(blindedPath)-1].CipherText
3✔
1452
                finalHop := route.Hop{
3✔
1453
                        AmtToForward:     amount,
3✔
1454
                        OutgoingTimeLock: uint32(finalHtlcExpiry),
3✔
1455
                        EncryptedData:    encryptedData,
3✔
1456
                }
3✔
1457
                if len(blindedPath) == 1 {
6✔
1458
                        finalHop.BlindingPoint = blindedPoint
3✔
1459
                }
3✔
1460

1461
                // The final hop does not have a short chanID set.
1462
                return finalHop.PayloadSize(0), nil
3✔
1463
        }
1464

1465
        var mpp *record.MPP
3✔
1466
        r.PaymentAddr.WhenSome(func(addr [32]byte) {
6✔
1467
                mpp = record.NewMPP(amount, addr)
3✔
1468
        })
3✔
1469

1470
        var amp *record.AMP
3✔
1471
        if r.Amp != nil {
6✔
1472
                // The AMP payload is not easy accessible at this point but we
3✔
1473
                // are only interested in the size of the payload so we just use
3✔
1474
                // the AMP record dummy.
3✔
1475
                amp = &record.MaxAmpPayLoadSize
3✔
1476
        }
3✔
1477

1478
        finalHop := route.Hop{
3✔
1479
                AmtToForward:     amount,
3✔
1480
                OutgoingTimeLock: uint32(finalHtlcExpiry),
3✔
1481
                CustomRecords:    r.DestCustomRecords,
3✔
1482
                MPP:              mpp,
3✔
1483
                AMP:              amp,
3✔
1484
                Metadata:         r.Metadata,
3✔
1485
        }
3✔
1486

3✔
1487
        // The final hop does not have a short chanID set.
3✔
1488
        return finalHop.PayloadSize(0), nil
3✔
1489
}
1490

1491
// overflowSafeAdd adds two MilliSatoshi values and returns the result. If an
1492
// overflow could occur, zero is returned instead and the boolean is set to
1493
// true.
1494
func overflowSafeAdd(x, y lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) {
3✔
1495
        if y > math.MaxUint64-x {
3✔
1496
                // Overflow would occur, return 0 and set overflow flag.
×
1497
                return 0, true
×
1498
        }
×
1499

1500
        return x + y, false
3✔
1501
}
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