• 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

77.61
/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/lntypes"
10
        "github.com/lightningnetwork/lnd/lnwire"
11
        "github.com/lightningnetwork/lnd/routing/route"
12
)
13

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

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

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

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

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

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

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

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

3✔
57
        localChan := fromNode == u.sourceNode
3✔
58

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

×
UNCOV
65
                        return
×
UNCOV
66
                }
×
67
        }
68

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

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

×
85
                return
×
86
        }
×
87

88
        // Zero inbound fee for exit hops.
89
        if !u.useInboundFees {
6✔
90
                inboundFee = models.InboundFee{}
3✔
91
        }
3✔
92

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

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

×
109
                        return nil
×
110
                }
×
111

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

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

3✔
125
                return nil
3✔
126
        }
127

128
        // Iterate over all channels of the to node.
129
        return g.ForEachNodeChannel(u.toNode, cb)
3✔
130
}
131

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

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

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

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

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

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

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

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

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

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

193
        return true
3✔
194
}
195

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

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

3✔
209
        if u.localChan {
6✔
210
                return u.getEdgeLocal(
3✔
211
                        netAmtReceived, bandwidthHints, nextOutFee,
3✔
212
                )
3✔
213
        }
3✔
214

215
        return u.getEdgeNetwork(netAmtReceived, nextOutFee)
3✔
216
}
217

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

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

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

232
        return inboundFee
3✔
233
}
234

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

3✔
241
        var (
3✔
242
                bestEdge     *unifiedEdge
3✔
243
                maxBandwidth lnwire.MilliSatoshi
3✔
244
        )
3✔
245

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

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

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

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

×
UNCOV
266
                        continue
×
267
                }
268

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

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

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

3✔
305
                        continue
3✔
306
                }
307

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

×
UNCOV
317
                        continue
×
318
                }
319
                maxBandwidth = bandwidth
3✔
320

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

328
        return bestEdge
3✔
329
}
330

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

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

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

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

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

363
                // For network channels, skip the disabled ones.
364
                edgeFlags := edge.policy.ChannelFlags
3✔
365
                isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0
3✔
366
                if isDisabled {
3✔
UNCOV
367
                        log.Debugf("Skipped edge %v due to it being disabled",
×
UNCOV
368
                                edge.policy.ChannelID)
×
UNCOV
369
                        continue
×
370
                }
371

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

×
UNCOV
380
                        capMsat = edge.policy.MaxHTLC
×
UNCOV
381
                }
×
382
                maxCapMsat = lntypes.Max(capMsat, maxCapMsat)
3✔
383

3✔
384
                // Track the maximum time lock of all channels that are
3✔
385
                // candidate for non-strict forwarding at the routing node.
3✔
386
                maxTimelock = lntypes.Max(
3✔
387
                        maxTimelock, edge.policy.TimeLockDelta,
3✔
388
                )
3✔
389

3✔
390
                outboundFee := int64(edge.policy.ComputeFee(amt))
3✔
391
                fee := outboundFee + inboundFee
3✔
392

3✔
393
                // Use the policy that results in the highest fee for this
3✔
394
                // specific amount.
3✔
395
                if fee < maxFee {
3✔
UNCOV
396
                        log.Debugf("Skipped edge %v due to it produces less "+
×
UNCOV
397
                                "fee: fee=%v, maxFee=%v",
×
UNCOV
398
                                edge.policy.ChannelID, fee, maxFee)
×
UNCOV
399

×
UNCOV
400
                        continue
×
401
                }
402
                maxFee = fee
3✔
403

3✔
404
                bestPolicy = newUnifiedEdge(
3✔
405
                        edge.policy, 0, edge.inboundFees, nil,
3✔
406
                        edge.blindedPayment,
3✔
407
                )
3✔
408

3✔
409
                // The payload size function for edges to a connected peer is
3✔
410
                // always the same hence there is not need to find the maximum.
3✔
411
                // This also counts for blinded edges where we only have one
3✔
412
                // edge to a blinded peer.
3✔
413
                hopPayloadSizeFn = edge.hopPayloadSizeFn
3✔
414
        }
415

416
        // Return early if no channel matches.
417
        if bestPolicy == nil {
6✔
418
                return nil
3✔
419
        }
3✔
420

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

3✔
438
        return modifiedEdge
3✔
439
}
440

441
// minAmt returns the minimum amount that can be forwarded on this connection.
UNCOV
442
func (u *edgeUnifier) minAmt() lnwire.MilliSatoshi {
×
UNCOV
443
        min := lnwire.MaxMilliSatoshi
×
UNCOV
444
        for _, edge := range u.edges {
×
UNCOV
445
                min = lntypes.Min(min, edge.policy.MinHTLC)
×
UNCOV
446
        }
×
447

UNCOV
448
        return min
×
449
}
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