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

lightningnetwork / lnd / 16918135633

12 Aug 2025 06:56PM UTC coverage: 56.955% (-9.9%) from 66.9%
16918135633

push

github

web-flow
Merge pull request #9871 from GeorgeTsagk/htlc-noop-add

Add `NoopAdd` HTLCs

48 of 147 new or added lines in 3 files covered. (32.65%)

29154 existing lines in 462 files now uncovered.

98265 of 172532 relevant lines covered (56.95%)

1.19 hits per line

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

79.41
/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 {
2✔
37

2✔
38
        return &nodeEdgeUnifier{
2✔
39
                edgeUnifiers:   make(map[route.Vertex]*edgeUnifier),
2✔
40
                toNode:         toNode,
2✔
41
                useInboundFees: useInboundFees,
2✔
42
                sourceNode:     sourceNode,
2✔
43
                outChanRestr:   outChanRestr,
2✔
44
        }
2✔
45
}
2✔
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) {
2✔
55

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

2✔
58
        // Skip channels if there is an outgoing channel restriction.
2✔
59
        if localChan && u.outChanRestr != nil {
2✔
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]
2✔
70
        if !ok {
4✔
71
                unifier = &edgeUnifier{
2✔
72
                        localChan: localChan,
2✔
73
                }
2✔
74
                u.edgeUnifiers[fromNode] = unifier
2✔
75
        }
2✔
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 {
2✔
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 {
4✔
89
                inboundFee = models.InboundFee{}
2✔
90
        }
2✔
91

92
        unifier.edges = append(unifier.edges, newUnifiedEdge(
2✔
93
                edge, capacity, inboundFee, hopPayloadSizeFn, blindedPayment,
2✔
94
        ))
2✔
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 {
2✔
100
        var channels []*graphdb.DirectedChannel
2✔
101
        cb := func(channel *graphdb.DirectedChannel) error {
4✔
102
                // If there is no edge policy for this candidate node, skip.
2✔
103
                // Note that we are searching backwards so this node would have
2✔
104
                // come prior to the pivot node in the route.
2✔
105
                if channel.InPolicy == nil {
2✔
106
                        log.Debugf("Skipped adding edge %v due to nil policy",
×
107
                                channel.ChannelID)
×
108

×
109
                        return nil
×
110
                }
×
111

112
                channels = append(channels, channel)
2✔
113

2✔
114
                return nil
2✔
115
        }
116

117
        // Iterate over all channels of the to node.
118
        err := g.ForEachNodeDirectedChannel(
2✔
119
                u.toNode, cb, func() {
4✔
120
                        channels = nil
2✔
121
                },
2✔
122
        )
123
        if err != nil {
2✔
124
                return err
×
125
        }
×
126

127
        for _, channel := range channels {
4✔
128
                // Add this policy to the corresponding edgeUnifier. We default
2✔
129
                // to the clear hop payload size function because
2✔
130
                // `addGraphPolicies` is only used for cleartext intermediate
2✔
131
                // hops in a route.
2✔
132
                inboundFee := models.NewInboundFeeFromWire(
2✔
133
                        channel.InboundFee,
2✔
134
                )
2✔
135

2✔
136
                u.addPolicy(
2✔
137
                        channel.OtherNode, channel.InPolicy, inboundFee,
2✔
138
                        channel.Capacity, defaultHopPayloadSize, nil,
2✔
139
                )
2✔
140
        }
2✔
141

142
        return nil
2✔
143
}
144

145
// unifiedEdge is the individual channel data that is kept inside an edgeUnifier
146
// object.
147
type unifiedEdge struct {
148
        policy      *models.CachedEdgePolicy
149
        capacity    btcutil.Amount
150
        inboundFees models.InboundFee
151

152
        // hopPayloadSize supplies an edge with the ability to calculate the
153
        // exact payload size if this edge would be included in a route. This
154
        // is needed because hops of a blinded path differ in their payload
155
        // structure compared to cleartext hops.
156
        hopPayloadSizeFn PayloadSizeFunc
157

158
        // blindedPayment if set, is the BlindedPayment that this edge was
159
        // derived from originally.
160
        blindedPayment *BlindedPayment
161
}
162

163
// newUnifiedEdge constructs a new unifiedEdge.
164
func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount,
165
        inboundFees models.InboundFee, hopPayloadSizeFn PayloadSizeFunc,
166
        blindedPayment *BlindedPayment) *unifiedEdge {
2✔
167

2✔
168
        return &unifiedEdge{
2✔
169
                policy:           policy,
2✔
170
                capacity:         capacity,
2✔
171
                inboundFees:      inboundFees,
2✔
172
                hopPayloadSizeFn: hopPayloadSizeFn,
2✔
173
                blindedPayment:   blindedPayment,
2✔
174
        }
2✔
175
}
2✔
176

177
// amtInRange checks whether an amount falls within the valid range for a
178
// channel.
179
func (u *unifiedEdge) amtInRange(amt lnwire.MilliSatoshi) bool {
2✔
180
        // If the capacity is available (non-light clients), skip channels that
2✔
181
        // are too small.
2✔
182
        if u.capacity > 0 &&
2✔
183
                amt > lnwire.NewMSatFromSatoshis(u.capacity) {
2✔
UNCOV
184

×
UNCOV
185
                log.Tracef("Not enough capacity: amt=%v, capacity=%v",
×
UNCOV
186
                        amt, u.capacity)
×
UNCOV
187
                return false
×
UNCOV
188
        }
×
189

190
        // Skip channels for which this htlc is too large.
191
        if u.policy.MessageFlags.HasMaxHtlc() &&
2✔
192
                amt > u.policy.MaxHTLC {
4✔
193

2✔
194
                log.Tracef("Exceeds policy's MaxHTLC: amt=%v, MaxHTLC=%v",
2✔
195
                        amt, u.policy.MaxHTLC)
2✔
196
                return false
2✔
197
        }
2✔
198

199
        // Skip channels for which this htlc is too small.
200
        if amt < u.policy.MinHTLC {
4✔
201
                log.Tracef("below policy's MinHTLC: amt=%v, MinHTLC=%v",
2✔
202
                        amt, u.policy.MinHTLC)
2✔
203
                return false
2✔
204
        }
2✔
205

206
        return true
2✔
207
}
208

209
// edgeUnifier is an object that covers all channels between a pair of nodes.
210
type edgeUnifier struct {
211
        edges     []*unifiedEdge
212
        localChan bool
213
}
214

215
// getEdge returns the optimal unified edge to use for this connection given a
216
// specific amount to send. It differentiates between local and network
217
// channels.
218
func (u *edgeUnifier) getEdge(netAmtReceived lnwire.MilliSatoshi,
219
        bandwidthHints bandwidthHints,
220
        nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
2✔
221

2✔
222
        if u.localChan {
4✔
223
                return u.getEdgeLocal(
2✔
224
                        netAmtReceived, bandwidthHints, nextOutFee,
2✔
225
                )
2✔
226
        }
2✔
227

228
        return u.getEdgeNetwork(netAmtReceived, nextOutFee)
2✔
229
}
230

231
// calcCappedInboundFee calculates the inbound fee for a channel, taking into
232
// account the total node fee for the "to" node.
233
func calcCappedInboundFee(edge *unifiedEdge, amt lnwire.MilliSatoshi,
234
        nextOutFee lnwire.MilliSatoshi) int64 {
2✔
235

2✔
236
        // Calculate the inbound fee charged for the amount that passes over the
2✔
237
        // channel.
2✔
238
        inboundFee := edge.inboundFees.CalcFee(amt)
2✔
239

2✔
240
        // Take into account that the total node fee cannot be negative.
2✔
241
        if inboundFee < -int64(nextOutFee) {
2✔
UNCOV
242
                inboundFee = -int64(nextOutFee)
×
UNCOV
243
        }
×
244

245
        return inboundFee
2✔
246
}
247

248
// getEdgeLocal returns the optimal unified edge to use for this local
249
// connection given a specific amount to send.
250
func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
251
        bandwidthHints bandwidthHints,
252
        nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
2✔
253

2✔
254
        var (
2✔
255
                bestEdge     *unifiedEdge
2✔
256
                maxBandwidth lnwire.MilliSatoshi
2✔
257
        )
2✔
258

2✔
259
        for _, edge := range u.edges {
4✔
260
                // Calculate the inbound fee charged at the receiving node.
2✔
261
                inboundFee := calcCappedInboundFee(
2✔
262
                        edge, netAmtReceived, nextOutFee,
2✔
263
                )
2✔
264

2✔
265
                // Add inbound fee to get to the amount that is sent over the
2✔
266
                // local channel.
2✔
267
                amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
2✔
268
                // Check valid amount range for the channel. We skip this test
2✔
269

2✔
270
                // for payments with custom htlc data we skip the amount range
2✔
271
                // check because the amt of the payment does not relate to the
2✔
272
                // actual amount carried by the HTLC but instead is encoded in
2✔
273
                // the blob data.
2✔
274
                if !bandwidthHints.isCustomHTLCPayment() &&
2✔
275
                        !edge.amtInRange(amt) {
4✔
276

2✔
277
                        log.Debugf("Amount %v not in range for edge %v",
2✔
278
                                netAmtReceived, edge.policy.ChannelID)
2✔
279

2✔
280
                        continue
2✔
281
                }
282

283
                // For local channels, there is no fee to pay or an extra time
284
                // lock. We only consider the currently available bandwidth for
285
                // channel selection. The disabled flag is ignored for local
286
                // channels.
287

288
                // Retrieve bandwidth for this local channel. If not
289
                // available, assume this channel has enough bandwidth.
290
                //
291
                // TODO(joostjager): Possibly change to skipping this
292
                // channel. The bandwidth hint is expected to be
293
                // available.
294
                bandwidth, ok := bandwidthHints.availableChanBandwidth(
2✔
295
                        edge.policy.ChannelID, amt,
2✔
296
                )
2✔
297
                if !ok {
2✔
UNCOV
298
                        log.Warnf("Cannot get bandwidth for edge %v, use max "+
×
UNCOV
299
                                "instead", edge.policy.ChannelID)
×
UNCOV
300

×
UNCOV
301
                        bandwidth = lnwire.MaxMilliSatoshi
×
UNCOV
302
                }
×
303

304
                // TODO(yy): if the above `!ok` is chosen, we'd have
305
                // `bandwidth` to be the max value, which will end up having
306
                // the `maxBandwidth` to be have the largest value and this
307
                // edge will be the chosen one. This is wrong in two ways,
308
                // 1. we need to understand why `availableChanBandwidth` cannot
309
                // find bandwidth for this edge as something is wrong with this
310
                // channel, and,
311
                // 2. this edge is likely NOT the local channel with the
312
                // highest available bandwidth.
313
                //
314
                // Skip channels that can't carry the payment.
315
                if amt > bandwidth {
4✔
316
                        log.Debugf("Skipped edge %v: not enough bandwidth, "+
2✔
317
                                "bandwidth=%v, amt=%v", edge.policy.ChannelID,
2✔
318
                                bandwidth, amt)
2✔
319

2✔
320
                        continue
2✔
321
                }
322

323
                // We pick the local channel with the highest available
324
                // bandwidth, to maximize the success probability. It can be
325
                // that the channel state changes between querying the bandwidth
326
                // hints and sending out the htlc.
327
                if bandwidth < maxBandwidth {
2✔
UNCOV
328
                        log.Debugf("Skipped edge %v: not max bandwidth, "+
×
UNCOV
329
                                "bandwidth=%v, maxBandwidth=%v",
×
UNCOV
330
                                edge.policy.ChannelID, bandwidth, maxBandwidth)
×
UNCOV
331

×
UNCOV
332
                        continue
×
333
                }
334
                maxBandwidth = bandwidth
2✔
335

2✔
336
                // Update best edge.
2✔
337
                bestEdge = newUnifiedEdge(
2✔
338
                        edge.policy, edge.capacity, edge.inboundFees,
2✔
339
                        edge.hopPayloadSizeFn, edge.blindedPayment,
2✔
340
                )
2✔
341
        }
342

343
        return bestEdge
2✔
344
}
345

346
// getEdgeNetwork returns the optimal unified edge to use for this connection
347
// given a specific amount to send. The goal is to return a unified edge with a
348
// policy that maximizes the probability of a successful forward in a non-strict
349
// forwarding context.
350
func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
351
        nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
2✔
352

2✔
353
        var (
2✔
354
                bestPolicy       *unifiedEdge
2✔
355
                maxFee           int64 = math.MinInt64
2✔
356
                maxTimelock      uint16
2✔
357
                maxCapMsat       lnwire.MilliSatoshi
2✔
358
                hopPayloadSizeFn PayloadSizeFunc
2✔
359
        )
2✔
360

2✔
361
        for _, edge := range u.edges {
4✔
362
                // Calculate the inbound fee charged at the receiving node.
2✔
363
                inboundFee := calcCappedInboundFee(
2✔
364
                        edge, netAmtReceived, nextOutFee,
2✔
365
                )
2✔
366

2✔
367
                // Add inbound fee to get to the amount that is sent over the
2✔
368
                // channel.
2✔
369
                amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
2✔
370

2✔
371
                // Check valid amount range for the channel.
2✔
372
                if !edge.amtInRange(amt) {
4✔
373
                        log.Debugf("Amount %v not in range for edge %v",
2✔
374
                                amt, edge.policy.ChannelID)
2✔
375
                        continue
2✔
376
                }
377

378
                // For network channels, skip the disabled ones.
379
                if edge.policy.IsDisabled() {
2✔
UNCOV
380
                        log.Debugf("Skipped edge %v due to it being disabled",
×
UNCOV
381
                                edge.policy.ChannelID)
×
UNCOV
382
                        continue
×
383
                }
384

385
                // Track the maximal capacity for usable channels. If we don't
386
                // know the capacity, we fall back to MaxHTLC.
387
                capMsat := lnwire.NewMSatFromSatoshis(edge.capacity)
2✔
388
                if capMsat == 0 && edge.policy.MessageFlags.HasMaxHtlc() {
2✔
UNCOV
389
                        log.Tracef("No capacity available for channel %v, "+
×
UNCOV
390
                                "using MaxHtlcMsat (%v) as a fallback.",
×
UNCOV
391
                                edge.policy.ChannelID, edge.policy.MaxHTLC)
×
UNCOV
392

×
UNCOV
393
                        capMsat = edge.policy.MaxHTLC
×
UNCOV
394
                }
×
395
                maxCapMsat = max(capMsat, maxCapMsat)
2✔
396

2✔
397
                // Track the maximum time lock of all channels that are
2✔
398
                // candidate for non-strict forwarding at the routing node.
2✔
399
                maxTimelock = max(maxTimelock, edge.policy.TimeLockDelta)
2✔
400

2✔
401
                outboundFee := int64(edge.policy.ComputeFee(amt))
2✔
402
                fee := outboundFee + inboundFee
2✔
403

2✔
404
                // Use the policy that results in the highest fee for this
2✔
405
                // specific amount.
2✔
406
                if fee < maxFee {
2✔
UNCOV
407
                        log.Debugf("Skipped edge %v due to it produces less "+
×
UNCOV
408
                                "fee: fee=%v, maxFee=%v",
×
UNCOV
409
                                edge.policy.ChannelID, fee, maxFee)
×
UNCOV
410

×
UNCOV
411
                        continue
×
412
                }
413
                maxFee = fee
2✔
414

2✔
415
                bestPolicy = newUnifiedEdge(
2✔
416
                        edge.policy, 0, edge.inboundFees, nil,
2✔
417
                        edge.blindedPayment,
2✔
418
                )
2✔
419

2✔
420
                // The payload size function for edges to a connected peer is
2✔
421
                // always the same hence there is not need to find the maximum.
2✔
422
                // This also counts for blinded edges where we only have one
2✔
423
                // edge to a blinded peer.
2✔
424
                hopPayloadSizeFn = edge.hopPayloadSizeFn
2✔
425
        }
426

427
        // Return early if no channel matches.
428
        if bestPolicy == nil {
4✔
429
                return nil
2✔
430
        }
2✔
431

432
        // We have already picked the highest fee that could be required for
433
        // non-strict forwarding. To also cover the case where a lower fee
434
        // channel requires a longer time lock, we modify the policy by setting
435
        // the maximum encountered time lock. Note that this results in a
436
        // synthetic policy that is not actually present on the routing node.
437
        //
438
        // The reason we do this, is that we try to maximize the chance that we
439
        // get forwarded. Because we penalize pair-wise, there won't be a second
440
        // chance for this node pair. But this is all only needed for nodes that
441
        // have distinct policies for channels to the same peer.
442
        policyCopy := *bestPolicy.policy
2✔
443
        policyCopy.TimeLockDelta = maxTimelock
2✔
444
        modifiedEdge := newUnifiedEdge(
2✔
445
                &policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees,
2✔
446
                hopPayloadSizeFn, bestPolicy.blindedPayment,
2✔
447
        )
2✔
448

2✔
449
        return modifiedEdge
2✔
450
}
451

452
// minAmt returns the minimum amount that can be forwarded on this connection.
UNCOV
453
func (u *edgeUnifier) minAmt() lnwire.MilliSatoshi {
×
UNCOV
454
        minAmount := lnwire.MaxMilliSatoshi
×
UNCOV
455
        for _, edge := range u.edges {
×
UNCOV
456
                minAmount = min(minAmount, edge.policy.MinHTLC)
×
UNCOV
457
        }
×
458

UNCOV
459
        return minAmount
×
460
}
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