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

lightningnetwork / lnd / 15838907453

24 Jun 2025 01:26AM UTC coverage: 57.079% (-11.1%) from 68.172%
15838907453

Pull #9982

github

web-flow
Merge e42780be2 into 45c15646c
Pull Request #9982: lnwire+lnwallet: add LocalNonces field for splice nonce coordination w/ taproot channels

103 of 167 new or added lines in 5 files covered. (61.68%)

30191 existing lines in 463 files now uncovered.

96331 of 168768 relevant lines covered (57.08%)

0.6 hits per line

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

77.25
/routing/unified_edges.go
1
package routing
2

3
import (
4
        "math"
5

6
        "github.com/btcsuite/btcd/btcutil"
7
        graphdb "github.com/lightningnetwork/lnd/graph/db"
8
        "github.com/lightningnetwork/lnd/graph/db/models"
9
        "github.com/lightningnetwork/lnd/lnwire"
10
        "github.com/lightningnetwork/lnd/routing/route"
11
)
12

13
// nodeEdgeUnifier holds all edge unifiers for connections towards a node.
14
type nodeEdgeUnifier struct {
15
        // edgeUnifiers contains an edge unifier for every from node.
16
        edgeUnifiers map[route.Vertex]*edgeUnifier
17

18
        // sourceNode is the sender of a payment. The rules to pick the final
19
        // policy are different for local channels.
20
        sourceNode route.Vertex
21

22
        // toNode is the node for which the edge unifiers are instantiated.
23
        toNode route.Vertex
24

25
        // useInboundFees indicates whether to take inbound fees into account.
26
        useInboundFees bool
27

28
        // outChanRestr is an optional outgoing channel restriction for the
29
        // local channel to use.
30
        outChanRestr map[uint64]struct{}
31
}
32

33
// newNodeEdgeUnifier instantiates a new nodeEdgeUnifier object. Channel
34
// policies can be added to this object.
35
func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, useInboundFees bool,
36
        outChanRestr map[uint64]struct{}) *nodeEdgeUnifier {
1✔
37

1✔
38
        return &nodeEdgeUnifier{
1✔
39
                edgeUnifiers:   make(map[route.Vertex]*edgeUnifier),
1✔
40
                toNode:         toNode,
1✔
41
                useInboundFees: useInboundFees,
1✔
42
                sourceNode:     sourceNode,
1✔
43
                outChanRestr:   outChanRestr,
1✔
44
        }
1✔
45
}
1✔
46

47
// addPolicy adds a single channel policy. Capacity may be zero if unknown
48
// (light clients). We expect a non-nil payload size function and will request a
49
// graceful shutdown if it is not provided as this indicates that edges are
50
// incorrectly specified.
51
func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
52
        edge *models.CachedEdgePolicy, inboundFee models.InboundFee,
53
        capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc,
54
        blindedPayment *BlindedPayment) {
1✔
55

1✔
56
        localChan := fromNode == u.sourceNode
1✔
57

1✔
58
        // Skip channels if there is an outgoing channel restriction.
1✔
59
        if localChan && u.outChanRestr != nil {
1✔
UNCOV
60
                if _, ok := u.outChanRestr[edge.ChannelID]; !ok {
×
UNCOV
61
                        log.Debugf("Skipped adding policy for restricted edge "+
×
UNCOV
62
                                "%v", edge.ChannelID)
×
UNCOV
63

×
UNCOV
64
                        return
×
UNCOV
65
                }
×
66
        }
67

68
        // Update the edgeUnifiers map.
69
        unifier, ok := u.edgeUnifiers[fromNode]
1✔
70
        if !ok {
2✔
71
                unifier = &edgeUnifier{
1✔
72
                        localChan: localChan,
1✔
73
                }
1✔
74
                u.edgeUnifiers[fromNode] = unifier
1✔
75
        }
1✔
76

77
        // In case no payload size function was provided a graceful shutdown
78
        // is requested, because this function is not used as intended.
79
        if hopPayloadSizeFn == nil {
1✔
80
                log.Criticalf("No payloadsize function was provided for the "+
×
81
                        "edge (chanid=%v) when adding it to the edge unifier "+
×
82
                        "of node: %v", edge.ChannelID, fromNode)
×
83

×
84
                return
×
85
        }
×
86

87
        // Zero inbound fee for exit hops.
88
        if !u.useInboundFees {
2✔
89
                inboundFee = models.InboundFee{}
1✔
90
        }
1✔
91

92
        unifier.edges = append(unifier.edges, newUnifiedEdge(
1✔
93
                edge, capacity, inboundFee, hopPayloadSizeFn, blindedPayment,
1✔
94
        ))
1✔
95
}
96

97
// addGraphPolicies adds all policies that are known for the toNode in the
98
// graph.
99
func (u *nodeEdgeUnifier) addGraphPolicies(g Graph) error {
1✔
100
        cb := func(channel *graphdb.DirectedChannel) error {
2✔
101
                // If there is no edge policy for this candidate node, skip.
1✔
102
                // Note that we are searching backwards so this node would have
1✔
103
                // come prior to the pivot node in the route.
1✔
104
                if channel.InPolicy == nil {
1✔
105
                        log.Debugf("Skipped adding edge %v due to nil policy",
×
106
                                channel.ChannelID)
×
107

×
108
                        return nil
×
109
                }
×
110

111
                // Add this policy to the corresponding edgeUnifier. We default
112
                // to the clear hop payload size function because
113
                // `addGraphPolicies` is only used for cleartext intermediate
114
                // hops in a route.
115
                inboundFee := models.NewInboundFeeFromWire(
1✔
116
                        channel.InboundFee,
1✔
117
                )
1✔
118

1✔
119
                u.addPolicy(
1✔
120
                        channel.OtherNode, channel.InPolicy, inboundFee,
1✔
121
                        channel.Capacity, defaultHopPayloadSize, nil,
1✔
122
                )
1✔
123

1✔
124
                return nil
1✔
125
        }
126

127
        // Iterate over all channels of the to node.
128
        return g.ForEachNodeDirectedChannel(u.toNode, cb)
1✔
129
}
130

131
// unifiedEdge is the individual channel data that is kept inside an edgeUnifier
132
// object.
133
type unifiedEdge struct {
134
        policy      *models.CachedEdgePolicy
135
        capacity    btcutil.Amount
136
        inboundFees models.InboundFee
137

138
        // hopPayloadSize supplies an edge with the ability to calculate the
139
        // exact payload size if this edge would be included in a route. This
140
        // is needed because hops of a blinded path differ in their payload
141
        // structure compared to cleartext hops.
142
        hopPayloadSizeFn PayloadSizeFunc
143

144
        // blindedPayment if set, is the BlindedPayment that this edge was
145
        // derived from originally.
146
        blindedPayment *BlindedPayment
147
}
148

149
// newUnifiedEdge constructs a new unifiedEdge.
150
func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount,
151
        inboundFees models.InboundFee, hopPayloadSizeFn PayloadSizeFunc,
152
        blindedPayment *BlindedPayment) *unifiedEdge {
1✔
153

1✔
154
        return &unifiedEdge{
1✔
155
                policy:           policy,
1✔
156
                capacity:         capacity,
1✔
157
                inboundFees:      inboundFees,
1✔
158
                hopPayloadSizeFn: hopPayloadSizeFn,
1✔
159
                blindedPayment:   blindedPayment,
1✔
160
        }
1✔
161
}
1✔
162

163
// amtInRange checks whether an amount falls within the valid range for a
164
// channel.
165
func (u *unifiedEdge) amtInRange(amt lnwire.MilliSatoshi) bool {
1✔
166
        // If the capacity is available (non-light clients), skip channels that
1✔
167
        // are too small.
1✔
168
        if u.capacity > 0 &&
1✔
169
                amt > lnwire.NewMSatFromSatoshis(u.capacity) {
1✔
UNCOV
170

×
UNCOV
171
                log.Tracef("Not enough capacity: amt=%v, capacity=%v",
×
UNCOV
172
                        amt, u.capacity)
×
UNCOV
173
                return false
×
UNCOV
174
        }
×
175

176
        // Skip channels for which this htlc is too large.
177
        if u.policy.MessageFlags.HasMaxHtlc() &&
1✔
178
                amt > u.policy.MaxHTLC {
2✔
179

1✔
180
                log.Tracef("Exceeds policy's MaxHTLC: amt=%v, MaxHTLC=%v",
1✔
181
                        amt, u.policy.MaxHTLC)
1✔
182
                return false
1✔
183
        }
1✔
184

185
        // Skip channels for which this htlc is too small.
186
        if amt < u.policy.MinHTLC {
2✔
187
                log.Tracef("below policy's MinHTLC: amt=%v, MinHTLC=%v",
1✔
188
                        amt, u.policy.MinHTLC)
1✔
189
                return false
1✔
190
        }
1✔
191

192
        return true
1✔
193
}
194

195
// edgeUnifier is an object that covers all channels between a pair of nodes.
196
type edgeUnifier struct {
197
        edges     []*unifiedEdge
198
        localChan bool
199
}
200

201
// getEdge returns the optimal unified edge to use for this connection given a
202
// specific amount to send. It differentiates between local and network
203
// channels.
204
func (u *edgeUnifier) getEdge(netAmtReceived lnwire.MilliSatoshi,
205
        bandwidthHints bandwidthHints,
206
        nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
1✔
207

1✔
208
        if u.localChan {
2✔
209
                return u.getEdgeLocal(
1✔
210
                        netAmtReceived, bandwidthHints, nextOutFee,
1✔
211
                )
1✔
212
        }
1✔
213

214
        return u.getEdgeNetwork(netAmtReceived, nextOutFee)
1✔
215
}
216

217
// calcCappedInboundFee calculates the inbound fee for a channel, taking into
218
// account the total node fee for the "to" node.
219
func calcCappedInboundFee(edge *unifiedEdge, amt lnwire.MilliSatoshi,
220
        nextOutFee lnwire.MilliSatoshi) int64 {
1✔
221

1✔
222
        // Calculate the inbound fee charged for the amount that passes over the
1✔
223
        // channel.
1✔
224
        inboundFee := edge.inboundFees.CalcFee(amt)
1✔
225

1✔
226
        // Take into account that the total node fee cannot be negative.
1✔
227
        if inboundFee < -int64(nextOutFee) {
1✔
UNCOV
228
                inboundFee = -int64(nextOutFee)
×
UNCOV
229
        }
×
230

231
        return inboundFee
1✔
232
}
233

234
// getEdgeLocal returns the optimal unified edge to use for this local
235
// connection given a specific amount to send.
236
func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
237
        bandwidthHints bandwidthHints,
238
        nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
1✔
239

1✔
240
        var (
1✔
241
                bestEdge     *unifiedEdge
1✔
242
                maxBandwidth lnwire.MilliSatoshi
1✔
243
        )
1✔
244

1✔
245
        for _, edge := range u.edges {
2✔
246
                // Calculate the inbound fee charged at the receiving node.
1✔
247
                inboundFee := calcCappedInboundFee(
1✔
248
                        edge, netAmtReceived, nextOutFee,
1✔
249
                )
1✔
250

1✔
251
                // Add inbound fee to get to the amount that is sent over the
1✔
252
                // local channel.
1✔
253
                amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
1✔
254

1✔
255
                // Check valid amount range for the channel. We skip this test
1✔
256
                // for payments with custom HTLC data, as the amount sent on
1✔
257
                // the BTC layer may differ from the amount that is actually
1✔
258
                // forwarded in custom channels.
1✔
259
                if bandwidthHints.firstHopCustomBlob().IsNone() &&
1✔
260
                        !edge.amtInRange(amt) {
1✔
UNCOV
261

×
UNCOV
262
                        log.Debugf("Amount %v not in range for edge %v",
×
UNCOV
263
                                netAmtReceived, edge.policy.ChannelID)
×
UNCOV
264

×
UNCOV
265
                        continue
×
266
                }
267

268
                // For local channels, there is no fee to pay or an extra time
269
                // lock. We only consider the currently available bandwidth for
270
                // channel selection. The disabled flag is ignored for local
271
                // channels.
272

273
                // Retrieve bandwidth for this local channel. If not
274
                // available, assume this channel has enough bandwidth.
275
                //
276
                // TODO(joostjager): Possibly change to skipping this
277
                // channel. The bandwidth hint is expected to be
278
                // available.
279
                bandwidth, ok := bandwidthHints.availableChanBandwidth(
1✔
280
                        edge.policy.ChannelID, amt,
1✔
281
                )
1✔
282
                if !ok {
1✔
UNCOV
283
                        log.Debugf("Cannot get bandwidth for edge %v, use max "+
×
UNCOV
284
                                "instead", edge.policy.ChannelID)
×
UNCOV
285
                        bandwidth = lnwire.MaxMilliSatoshi
×
UNCOV
286
                }
×
287

288
                // TODO(yy): if the above `!ok` is chosen, we'd have
289
                // `bandwidth` to be the max value, which will end up having
290
                // the `maxBandwidth` to be have the largest value and this
291
                // edge will be the chosen one. This is wrong in two ways,
292
                // 1. we need to understand why `availableChanBandwidth` cannot
293
                // find bandwidth for this edge as something is wrong with this
294
                // channel, and,
295
                // 2. this edge is likely NOT the local channel with the
296
                // highest available bandwidth.
297
                //
298
                // Skip channels that can't carry the payment.
299
                if amt > bandwidth {
2✔
300
                        log.Debugf("Skipped edge %v: not enough bandwidth, "+
1✔
301
                                "bandwidth=%v, amt=%v", edge.policy.ChannelID,
1✔
302
                                bandwidth, amt)
1✔
303

1✔
304
                        continue
1✔
305
                }
306

307
                // We pick the local channel with the highest available
308
                // bandwidth, to maximize the success probability. It can be
309
                // that the channel state changes between querying the bandwidth
310
                // hints and sending out the htlc.
311
                if bandwidth < maxBandwidth {
1✔
UNCOV
312
                        log.Debugf("Skipped edge %v: not max bandwidth, "+
×
UNCOV
313
                                "bandwidth=%v, maxBandwidth=%v",
×
UNCOV
314
                                edge.policy.ChannelID, bandwidth, maxBandwidth)
×
UNCOV
315

×
UNCOV
316
                        continue
×
317
                }
318
                maxBandwidth = bandwidth
1✔
319

1✔
320
                // Update best edge.
1✔
321
                bestEdge = newUnifiedEdge(
1✔
322
                        edge.policy, edge.capacity, edge.inboundFees,
1✔
323
                        edge.hopPayloadSizeFn, edge.blindedPayment,
1✔
324
                )
1✔
325
        }
326

327
        return bestEdge
1✔
328
}
329

330
// getEdgeNetwork returns the optimal unified edge to use for this connection
331
// given a specific amount to send. The goal is to return a unified edge with a
332
// policy that maximizes the probability of a successful forward in a non-strict
333
// forwarding context.
334
func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
335
        nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
1✔
336

1✔
337
        var (
1✔
338
                bestPolicy       *unifiedEdge
1✔
339
                maxFee           int64 = math.MinInt64
1✔
340
                maxTimelock      uint16
1✔
341
                maxCapMsat       lnwire.MilliSatoshi
1✔
342
                hopPayloadSizeFn PayloadSizeFunc
1✔
343
        )
1✔
344

1✔
345
        for _, edge := range u.edges {
2✔
346
                // Calculate the inbound fee charged at the receiving node.
1✔
347
                inboundFee := calcCappedInboundFee(
1✔
348
                        edge, netAmtReceived, nextOutFee,
1✔
349
                )
1✔
350

1✔
351
                // Add inbound fee to get to the amount that is sent over the
1✔
352
                // channel.
1✔
353
                amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
1✔
354

1✔
355
                // Check valid amount range for the channel.
1✔
356
                if !edge.amtInRange(amt) {
2✔
357
                        log.Debugf("Amount %v not in range for edge %v",
1✔
358
                                amt, edge.policy.ChannelID)
1✔
359
                        continue
1✔
360
                }
361

362
                // For network channels, skip the disabled ones.
363
                if edge.policy.IsDisabled() {
1✔
UNCOV
364
                        log.Debugf("Skipped edge %v due to it being disabled",
×
UNCOV
365
                                edge.policy.ChannelID)
×
UNCOV
366
                        continue
×
367
                }
368

369
                // Track the maximal capacity for usable channels. If we don't
370
                // know the capacity, we fall back to MaxHTLC.
371
                capMsat := lnwire.NewMSatFromSatoshis(edge.capacity)
1✔
372
                if capMsat == 0 && edge.policy.MessageFlags.HasMaxHtlc() {
1✔
UNCOV
373
                        log.Tracef("No capacity available for channel %v, "+
×
UNCOV
374
                                "using MaxHtlcMsat (%v) as a fallback.",
×
UNCOV
375
                                edge.policy.ChannelID, edge.policy.MaxHTLC)
×
UNCOV
376

×
UNCOV
377
                        capMsat = edge.policy.MaxHTLC
×
UNCOV
378
                }
×
379
                maxCapMsat = max(capMsat, maxCapMsat)
1✔
380

1✔
381
                // Track the maximum time lock of all channels that are
1✔
382
                // candidate for non-strict forwarding at the routing node.
1✔
383
                maxTimelock = max(maxTimelock, edge.policy.TimeLockDelta)
1✔
384

1✔
385
                outboundFee := int64(edge.policy.ComputeFee(amt))
1✔
386
                fee := outboundFee + inboundFee
1✔
387

1✔
388
                // Use the policy that results in the highest fee for this
1✔
389
                // specific amount.
1✔
390
                if fee < maxFee {
1✔
UNCOV
391
                        log.Debugf("Skipped edge %v due to it produces less "+
×
UNCOV
392
                                "fee: fee=%v, maxFee=%v",
×
UNCOV
393
                                edge.policy.ChannelID, fee, maxFee)
×
UNCOV
394

×
UNCOV
395
                        continue
×
396
                }
397
                maxFee = fee
1✔
398

1✔
399
                bestPolicy = newUnifiedEdge(
1✔
400
                        edge.policy, 0, edge.inboundFees, nil,
1✔
401
                        edge.blindedPayment,
1✔
402
                )
1✔
403

1✔
404
                // The payload size function for edges to a connected peer is
1✔
405
                // always the same hence there is not need to find the maximum.
1✔
406
                // This also counts for blinded edges where we only have one
1✔
407
                // edge to a blinded peer.
1✔
408
                hopPayloadSizeFn = edge.hopPayloadSizeFn
1✔
409
        }
410

411
        // Return early if no channel matches.
412
        if bestPolicy == nil {
2✔
413
                return nil
1✔
414
        }
1✔
415

416
        // We have already picked the highest fee that could be required for
417
        // non-strict forwarding. To also cover the case where a lower fee
418
        // channel requires a longer time lock, we modify the policy by setting
419
        // the maximum encountered time lock. Note that this results in a
420
        // synthetic policy that is not actually present on the routing node.
421
        //
422
        // The reason we do this, is that we try to maximize the chance that we
423
        // get forwarded. Because we penalize pair-wise, there won't be a second
424
        // chance for this node pair. But this is all only needed for nodes that
425
        // have distinct policies for channels to the same peer.
426
        policyCopy := *bestPolicy.policy
1✔
427
        policyCopy.TimeLockDelta = maxTimelock
1✔
428
        modifiedEdge := newUnifiedEdge(
1✔
429
                &policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees,
1✔
430
                hopPayloadSizeFn, bestPolicy.blindedPayment,
1✔
431
        )
1✔
432

1✔
433
        return modifiedEdge
1✔
434
}
435

436
// minAmt returns the minimum amount that can be forwarded on this connection.
UNCOV
437
func (u *edgeUnifier) minAmt() lnwire.MilliSatoshi {
×
UNCOV
438
        minAmount := lnwire.MaxMilliSatoshi
×
UNCOV
439
        for _, edge := range u.edges {
×
UNCOV
440
                minAmount = min(minAmount, edge.policy.MinHTLC)
×
UNCOV
441
        }
×
442

UNCOV
443
        return minAmount
×
444
}
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