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

lightningnetwork / lnd / 14779943269

01 May 2025 05:22PM UTC coverage: 69.041% (+0.009%) from 69.032%
14779943269

Pull #9752

github

web-flow
Merge 51b1059cc into b068d79df
Pull Request #9752: routerrpc: reject payment to invoice that don't have payment secret or blinded paths

5 of 6 new or added lines in 1 file covered. (83.33%)

241 existing lines in 16 files now uncovered.

133931 of 193989 relevant lines covered (69.04%)

22108.46 hits per line

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

89.19
/routing/result_interpretation.go
1
package routing
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "io"
7

8
        "github.com/lightningnetwork/lnd/channeldb"
9
        "github.com/lightningnetwork/lnd/fn/v2"
10
        "github.com/lightningnetwork/lnd/lnwire"
11
        "github.com/lightningnetwork/lnd/routing/route"
12
        "github.com/lightningnetwork/lnd/tlv"
13
)
14

15
// Instantiate variables to allow taking a reference from the failure reason.
16
var (
17
        reasonError            = channeldb.FailureReasonError
18
        reasonIncorrectDetails = channeldb.FailureReasonPaymentDetails
19
)
20

21
// pairResult contains the result of the interpretation of a payment attempt for
22
// a specific node pair.
23
type pairResult struct {
24
        // amt is the amount that was forwarded for this pair. Can be set to
25
        // zero for failures that are amount independent.
26
        amt lnwire.MilliSatoshi
27

28
        // success indicates whether the payment attempt was successful through
29
        // this pair.
30
        success bool
31
}
32

33
// failPairResult creates a new result struct for a failure.
34
func failPairResult(minPenalizeAmt lnwire.MilliSatoshi) pairResult {
186✔
35
        return pairResult{
186✔
36
                amt: minPenalizeAmt,
186✔
37
        }
186✔
38
}
186✔
39

40
// newSuccessPairResult creates a new result struct for a success.
41
func successPairResult(successAmt lnwire.MilliSatoshi) pairResult {
138✔
42
        return pairResult{
138✔
43
                success: true,
138✔
44
                amt:     successAmt,
138✔
45
        }
138✔
46
}
138✔
47

48
// String returns the human-readable representation of a pair result.
49
func (p pairResult) String() string {
×
50
        var resultType string
×
51
        if p.success {
×
52
                resultType = "success"
×
53
        } else {
×
54
                resultType = "failed"
×
55
        }
×
56

57
        return fmt.Sprintf("%v (amt=%v)", resultType, p.amt)
×
58
}
59

60
// interpretedResult contains the result of the interpretation of a payment
61
// attempt.
62
type interpretedResult struct {
63
        // nodeFailure points to a node pubkey if all channels of that node are
64
        // responsible for the result.
65
        nodeFailure *route.Vertex
66

67
        // pairResults contains a map of node pairs for which we have a result.
68
        pairResults map[DirectedNodePair]pairResult
69

70
        // finalFailureReason is set to a non-nil value if it makes no more
71
        // sense to start another payment attempt. It will contain the reason
72
        // why.
73
        finalFailureReason *channeldb.FailureReason
74

75
        // policyFailure is set to a node pair if there is a policy failure on
76
        // that connection. This is used to control the second chance logic for
77
        // policy failures.
78
        policyFailure *DirectedNodePair
79
}
80

81
// interpretResult interprets a payment outcome and returns an object that
82
// contains information required to update mission control.
83
func interpretResult(rt *mcRoute,
84
        failure fn.Option[paymentFailure]) *interpretedResult {
101✔
85

101✔
86
        i := &interpretedResult{
101✔
87
                pairResults: make(map[DirectedNodePair]pairResult),
101✔
88
        }
101✔
89

101✔
90
        return fn.ElimOption(failure, func() *interpretedResult {
130✔
91
                i.processSuccess(rt)
29✔
92

29✔
93
                return i
29✔
94
        }, func(info paymentFailure) *interpretedResult {
104✔
95
                i.processFail(rt, info)
75✔
96

75✔
97
                return i
75✔
98
        })
75✔
99
}
100

101
// processSuccess processes a successful payment attempt.
102
func (i *interpretedResult) processSuccess(route *mcRoute) {
29✔
103
        // For successes, all nodes must have acted in the right way.
29✔
104
        // Therefore we mark all of them with a success result. However we need
29✔
105
        // to handle the blinded route part separately because for intermediate
29✔
106
        // blinded nodes the amount field is set to zero so we use the receiver
29✔
107
        // amount.
29✔
108
        introIdx, isBlinded := introductionPointIndex(route)
29✔
109
        if isBlinded {
33✔
110
                // Report success for all the pairs until the introduction
4✔
111
                // point.
4✔
112
                i.successPairRange(route, 0, introIdx-1)
4✔
113

4✔
114
                // Handle the blinded route part.
4✔
115
                //
4✔
116
                // NOTE: The introIdx index here does describe the node after
4✔
117
                // the introduction point.
4✔
118
                i.markBlindedRouteSuccess(route, introIdx)
4✔
119

4✔
120
                return
4✔
121
        }
4✔
122

123
        // Mark nodes as successful in the non-blinded case of the payment.
124
        i.successPairRange(route, 0, len(route.hops.Val)-1)
28✔
125
}
126

127
// processFail processes a failed payment attempt.
128
func (i *interpretedResult) processFail(rt *mcRoute, failure paymentFailure) {
75✔
129
        if failure.info.IsNone() {
75✔
130
                i.processPaymentOutcomeUnknown(rt)
75✔
131
                return
76✔
132
        }
1✔
133

1✔
134
        var (
1✔
135
                idx     int
136
                failMsg lnwire.FailureMessage
74✔
137
        )
74✔
138

74✔
139
        failure.info.WhenSome(
74✔
140
                func(r tlv.RecordT[tlv.TlvType0, paymentFailureInfo]) {
74✔
141
                        idx = int(r.Val.sourceIdx.Val)
74✔
142
                        failMsg = r.Val.msg.Val.FailureMessage
148✔
143
                },
74✔
144
        )
74✔
145

74✔
146
        // If the payment was to a blinded route and we received an error from
74✔
147
        // after the introduction point, handle this error separately - there
147✔
148
        // has been a protocol violation from the introduction node. This
73✔
149
        // penalty applies regardless of the error code that is returned.
73✔
150
        introIdx, isBlinded := introductionPointIndex(rt)
73✔
151
        if isBlinded && introIdx < idx {
152
                i.processPaymentOutcomeBadIntro(rt, introIdx, idx)
153
                return
154
        }
155

156
        switch idx {
157
        // We are the source of the failure.
158
        case 0:
159
                i.processPaymentOutcomeSelf(rt, failMsg)
74✔
160

76✔
161
        // A failure from the final hop was received.
2✔
162
        case len(rt.hops.Val):
2✔
163
                i.processPaymentOutcomeFinal(rt, failMsg)
2✔
164

165
        // An intermediate hop failed. Interpret the outcome, update reputation
72✔
166
        // and try again.
167
        default:
5✔
168
                i.processPaymentOutcomeIntermediate(rt, idx, failMsg)
5✔
169
        }
170
}
171

12✔
172
// processPaymentOutcomeBadIntro handles the case where we have made payment
12✔
173
// to a blinded route, but received an error from a node after the introduction
174
// node. This indicates that the introduction node is not obeying the route
175
// blinding specification, as we expect all errors from the introduction node
176
// to be source from it.
61✔
177
func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute,
61✔
178
        introIdx, errSourceIdx int) {
179

180
        // We fail the introduction node for not obeying the specification.
181
        i.failNode(route, introIdx)
182

183
        // Other preceding channels in the route forwarded correctly. Note
184
        // that we do not assign success to the incoming link to the
185
        // introduction node because it has not handled the error correctly.
186
        if introIdx > 1 {
187
                i.successPairRange(route, 0, introIdx-2)
2✔
188
        }
2✔
189

2✔
190
        // If the source of the failure was from the final node, we also set
2✔
191
        // a final failure reason because the recipient can't process the
2✔
192
        // payment (independent of the introduction failing to convert the
2✔
193
        // error, we can't complete the payment if the last hop fails).
2✔
194
        if errSourceIdx == len(route.hops.Val) {
2✔
195
                i.finalFailureReason = &reasonError
3✔
196
        }
1✔
197
}
1✔
198

199
// processPaymentOutcomeSelf handles failures sent by ourselves.
200
func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute,
201
        failure lnwire.FailureMessage) {
202

203
        switch failure.(type) {
3✔
204

1✔
205
        // We receive a malformed htlc failure from our peer. We trust ourselves
1✔
206
        // to send the correct htlc, so our peer must be at fault.
207
        case *lnwire.FailInvalidOnionVersion,
208
                *lnwire.FailInvalidOnionHmac,
209
                *lnwire.FailInvalidOnionKey:
210

5✔
211
                i.failNode(rt, 1)
5✔
212

5✔
213
                // If this was a payment to a direct peer, we can stop trying.
214
                if len(rt.hops.Val) == 1 {
215
                        i.finalFailureReason = &reasonError
216
                }
217

218
        // Any other failure originating from ourselves should be temporary and
2✔
219
        // caused by changing conditions between path finding and execution of
2✔
220
        // the payment. We just retry and trust that the information locally
2✔
221
        // available in the link has been updated.
2✔
222
        default:
2✔
223
                log.Warnf("Routing failure for local channel %v occurred",
3✔
224
                        rt.hops.Val[0].channelID)
1✔
225
        }
1✔
226
}
227

228
// processPaymentOutcomeFinal handles failures sent by the final hop.
229
func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute,
230
        failure lnwire.FailureMessage) {
231

3✔
232
        n := len(route.hops.Val)
3✔
233

3✔
234
        failNode := func() {
235
                i.failNode(route, n)
236

237
                // Other channels in the route forwarded correctly.
238
                if n > 1 {
239
                        i.successPairRange(route, 0, n-2)
12✔
240
                }
12✔
241

12✔
242
                i.finalFailureReason = &reasonError
12✔
243
        }
17✔
244

5✔
245
        // If a failure from the final node is received, we will fail the
5✔
246
        // payment in almost all cases. Only when the penultimate node sends an
5✔
247
        // incorrect htlc, we want to retry via another route. Invalid onion
9✔
248
        // failures are not expected, because the final node wouldn't be able to
4✔
249
        // encrypt that failure.
4✔
250
        switch failure.(type) {
251

5✔
252
        // Expiry or amount of the HTLC doesn't match the onion, try another
253
        // route.
254
        case *lnwire.FailFinalIncorrectCltvExpiry,
255
                *lnwire.FailFinalIncorrectHtlcAmount:
256

257
                // We trust ourselves. If this is a direct payment, we penalize
258
                // the final node and fail the payment.
259
                if n == 1 {
12✔
260
                        i.failNode(route, n)
261
                        i.finalFailureReason = &reasonError
262

263
                        return
264
                }
×
UNCOV
265

×
UNCOV
266
                // Otherwise penalize the last pair of the route and retry.
×
UNCOV
267
                // Either the final node is at fault, or it gets sent a bad htlc
×
UNCOV
268
                // from its predecessor.
×
269
                i.failPair(route, n-1)
×
270

×
271
                // The other hops relayed correctly, so assign those pairs a
×
272
                // success result. At this point, n >= 2.
×
273
                i.successPairRange(route, 0, n-2)
×
274

275
        // We are using wrong payment hash or amount, fail the payment.
276
        case *lnwire.FailIncorrectPaymentAmount,
277
                *lnwire.FailIncorrectDetails:
UNCOV
278

×
UNCOV
279
                // Assign all pairs a success result, as the payment reached the
×
UNCOV
280
                // destination correctly.
×
UNCOV
281
                i.successPairRange(route, 0, n-1)
×
UNCOV
282

×
283
                i.finalFailureReason = &reasonIncorrectDetails
284

285
        // The HTLC that was extended to the final hop expires too soon. Fail
286
        // the payment, because we may be using the wrong final cltv delta.
5✔
287
        case *lnwire.FailFinalExpiryTooSoon:
5✔
288
                // TODO(roasbeef): can happen to to race condition, try again
5✔
289
                // with recent block height
5✔
290

5✔
291
                // TODO(joostjager): can also happen because a node delayed
5✔
292
                // deliberately. What to penalize?
5✔
293
                i.finalFailureReason = &reasonIncorrectDetails
294

295
        case *lnwire.FailMPPTimeout:
UNCOV
296
                // Assign all pairs a success result, as the payment reached the
×
UNCOV
297
                // destination correctly. Continue the payment process.
×
UNCOV
298
                i.successPairRange(route, 0, n-1)
×
UNCOV
299

×
UNCOV
300
        // We do not expect to receive an invalid blinding error from the final
×
UNCOV
301
        // node in the route. This could erroneously happen in the following
×
UNCOV
302
        // cases:
×
303
        // 1. Unblinded node: misuses the error code.
304
        // 2. A receiving introduction node: erroneously sends the error code,
2✔
305
        //    as the spec indicates that receiving introduction nodes should
2✔
306
        //    use regular errors.
2✔
307
        //
2✔
308
        // Note that we expect the case where this error is sent from a node
309
        // after the introduction node to be handled elsewhere as this is part
310
        // of a more general class of errors where the introduction node has
311
        // failed to convert errors for the blinded route.
312
        case *lnwire.FailInvalidBlinding:
313
                failNode()
314

315
        // All other errors are considered terminal if coming from the
316
        // final hop. They indicate that something is wrong at the
317
        // recipient, so we do apply a penalty.
318
        default:
319
                failNode()
320
        }
321
}
2✔
322

2✔
323
// processPaymentOutcomeIntermediate handles failures sent by an intermediate
324
// hop.
325
//
326
//nolint:funlen
327
func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
3✔
328
        errorSourceIdx int, failure lnwire.FailureMessage) {
3✔
329

330
        reportOutgoing := func() {
331
                i.failPair(
332
                        route, errorSourceIdx,
333
                )
334
        }
335

336
        reportOutgoingBalance := func() {
337
                i.failPairBalance(
61✔
338
                        route, errorSourceIdx,
61✔
339
                )
67✔
340

6✔
341
                // All nodes up to the failing pair must have forwarded
6✔
342
                // successfully.
6✔
343
                i.successPairRange(route, 0, errorSourceIdx-1)
6✔
344
        }
345

98✔
346
        reportIncoming := func() {
37✔
347
                // We trust ourselves. If the error comes from the first hop, we
37✔
348
                // can penalize the whole node. In that case there is no
37✔
349
                // uncertainty as to which node to blame.
37✔
350
                if errorSourceIdx == 1 {
37✔
351
                        i.failNode(route, errorSourceIdx)
37✔
352
                        return
37✔
353
                }
37✔
354

355
                // Otherwise report the incoming pair.
74✔
356
                i.failPair(
13✔
357
                        route, errorSourceIdx-1,
13✔
358
                )
13✔
359

25✔
360
                // All nodes up to the failing pair must have forwarded
12✔
361
                // successfully.
12✔
362
                if errorSourceIdx > 1 {
12✔
363
                        i.successPairRange(route, 0, errorSourceIdx-2)
364
                }
365
        }
1✔
366

1✔
367
        reportNode := func() {
1✔
368
                // Fail only the node that reported the failure.
1✔
369
                i.failNode(route, errorSourceIdx)
1✔
370

1✔
371
                // Other preceding channels in the route forwarded correctly.
2✔
372
                if errorSourceIdx > 1 {
1✔
373
                        i.successPairRange(route, 0, errorSourceIdx-2)
1✔
374
                }
375
        }
376

65✔
377
        reportAll := func() {
4✔
378
                // We trust ourselves. If the error comes from the first hop, we
4✔
379
                // can penalize the whole node. In that case there is no
4✔
380
                // uncertainty as to which node to blame.
4✔
381
                if errorSourceIdx == 1 {
6✔
382
                        i.failNode(route, errorSourceIdx)
2✔
383
                        return
2✔
384
                }
385

386
                // Otherwise penalize all pairs up to the error source. This
64✔
387
                // includes our own outgoing connection.
3✔
388
                i.failPairRange(
3✔
389
                        route, 0, errorSourceIdx-1,
3✔
390
                )
5✔
391
        }
2✔
392

2✔
393
        switch failure.(type) {
2✔
394

395
        // If a node reports onion payload corruption or an invalid version,
396
        // that node may be responsible, but it could also be that it is just
397
        // relaying a malformed htlc failure from it successor. By reporting the
1✔
398
        // outgoing channel set, we will surely hit the responsible node. At
1✔
399
        // this point, it is not possible that the node's predecessor corrupted
1✔
400
        // the onion blob. If the predecessor would have corrupted the payload,
401
        // the error source wouldn't have been able to encrypt this failure
402
        // message for us.
61✔
403
        case *lnwire.FailInvalidOnionVersion,
404
                *lnwire.FailInvalidOnionHmac,
405
                *lnwire.FailInvalidOnionKey:
406

407
                reportOutgoing()
408

409
        // If InvalidOnionPayload is received, we penalize only the reporting
410
        // node. We know the preceding hop didn't corrupt the onion, since the
411
        // reporting node is able to send the failure. We assume that we
412
        // constructed a valid onion payload and that the failure is most likely
413
        // an unknown required type or a bug in their implementation.
414
        case *lnwire.InvalidOnionPayload:
3✔
415
                reportNode()
3✔
416

3✔
417
        // If the next hop in the route wasn't known or offline, we'll only
418
        // penalize the channel set which we attempted to route over. This is
419
        // conservative, and it can handle faulty channels between nodes
420
        // properly. Additionally, this guards against routing nodes returning
421
        // errors in order to attempt to black list another node.
422
        case *lnwire.FailUnknownNextPeer:
423
                reportOutgoing()
1✔
424

1✔
425
        // Some implementations use this error when the next hop is offline, so we
426
        // do the same as FailUnknownNextPeer and also process the channel update.
427
        case *lnwire.FailChannelDisabled:
428

429
                // Set the node pair for which a channel update may be out of
430
                // date. The second chance logic uses the policyFailure field.
431
                i.policyFailure = &DirectedNodePair{
5✔
432
                        From: route.hops.Val[errorSourceIdx-1].pubKeyBytes.Val,
5✔
433
                        To:   route.hops.Val[errorSourceIdx].pubKeyBytes.Val,
434
                }
435

436
                reportOutgoing()
4✔
437

4✔
438
                // All nodes up to the failing pair must have forwarded
4✔
439
                // successfully.
4✔
440
                i.successPairRange(route, 0, errorSourceIdx-1)
4✔
441

4✔
442
        // If we get a permanent channel, we'll prune the channel set in both
4✔
443
        // directions and continue with the rest of the routes.
4✔
444
        case *lnwire.FailPermanentChannelFailure:
4✔
445
                reportOutgoing()
4✔
446

4✔
447
        // When an HTLC parameter is incorrect, the node sending the error may
4✔
448
        // be doing something wrong. But it could also be that its predecessor
4✔
449
        // is intentionally modifying the htlc parameters that we instructed it
4✔
450
        // via the hop payload. Therefore we penalize the incoming node pair. A
451
        // third cause of this error may be that we have an out of date channel
452
        // update. This is handled by the second chance logic up in mission
453
        // control.
3✔
454
        case *lnwire.FailAmountBelowMinimum,
3✔
455
                *lnwire.FailFeeInsufficient,
456
                *lnwire.FailIncorrectCltvExpiry:
457

458
                // Set the node pair for which a channel update may be out of
459
                // date. The second chance logic uses the policyFailure field.
460
                i.policyFailure = &DirectedNodePair{
461
                        From: route.hops.Val[errorSourceIdx-1].pubKeyBytes.Val,
462
                        To:   route.hops.Val[errorSourceIdx].pubKeyBytes.Val,
463
                }
464

465
                // We report incoming channel. If a second pair is granted in
13✔
466
                // mission control, this report is ignored.
13✔
467
                reportIncoming()
13✔
468

13✔
469
        // If the outgoing channel doesn't have enough capacity, we penalize.
13✔
470
        // But we penalize only in a single direction and only for amounts
13✔
471
        // greater than the attempted amount.
13✔
472
        case *lnwire.FailTemporaryChannelFailure:
13✔
473
                reportOutgoingBalance()
13✔
474

13✔
475
        // If FailExpiryTooSoon is received, there must have been some delay
13✔
476
        // along the path. We can't know which node is causing the delay, so we
13✔
477
        // penalize all of them up to the error source.
478
        //
479
        // Alternatively it could also be that we ourselves have fallen behind
480
        // somehow. We ignore that case for now.
481
        case *lnwire.FailExpiryTooSoon:
37✔
482
                reportAll()
37✔
483

484
        // We only expect to get FailInvalidBlinding from an introduction node
485
        // in a blinded route. The introduction node in a blinded route is
486
        // always responsible for reporting errors for the blinded portion of
487
        // the route (to protect the privacy of the members of the route), so
488
        // we need to be careful not to unfairly "shoot the messenger".
489
        //
490
        // The introduction node has no incentive to falsely report errors to
3✔
491
        // sabotage the blinded route because:
3✔
492
        //   1. Its ability to route this payment is strictly tied to the
493
        //      blinded route.
494
        //   2. The pubkeys in the blinded route are ephemeral, so doing so
495
        //      will have no impact on the nodes beyond the individual payment.
496
        //
497
        // Here we handle a few cases where we could unexpectedly receive this
498
        // error:
499
        // 1. Outside of a blinded route: erring node is not spec compliant.
500
        // 2. Before the introduction point: erring node is not spec compliant.
501
        //
502
        // Note that we expect the case where this error is sent from a node
503
        // after the introduction node to be handled elsewhere as this is part
504
        // of a more general class of errors where the introduction node has
505
        // failed to convert errors for the blinded route.
506
        case *lnwire.FailInvalidBlinding:
507
                introIdx, isBlinded := introductionPointIndex(route)
508

509
                // Deal with cases where a node has incorrectly returned a
510
                // blinding error:
511
                // 1. A node before the introduction point returned it.
512
                // 2. A node in a non-blinded route returned it.
513
                if errorSourceIdx < introIdx || !isBlinded {
514
                        reportNode()
515
                        return
9✔
516
                }
9✔
517

9✔
518
                // Otherwise, the error was at the introduction node. All
9✔
519
                // nodes up until the introduction node forwarded correctly,
9✔
520
                // so we award them as successful.
9✔
521
                if introIdx >= 1 {
9✔
522
                        i.successPairRange(route, 0, introIdx-1)
12✔
523
                }
3✔
524

3✔
525
                // If the hop after the introduction node that sent us an
3✔
526
                // error is the final recipient, then we finally fail the
527
                // payment because the receiver has generated a blinded route
528
                // that they're unable to use. We have this special case so
529
                // that we don't penalize the introduction node, and there is
530
                // no point in retrying the payment while LND only supports
12✔
531
                // one blinded route per payment.
6✔
532
                //
6✔
533
                // Note that if LND is extended to support multiple blinded
534
                // routes, this will terminate the payment without re-trying
535
                // the other routes.
536
                if introIdx == len(route.hops.Val)-1 {
537
                        i.finalFailureReason = &reasonError
538
                } else {
539
                        // We penalize the final hop of the blinded route which
540
                        // is sufficient to not reuse this route again and is
541
                        // also more memory efficient because the other hops
542
                        // of the blinded path are ephemeral and will only be
543
                        // used in conjunction with the final hop. Moreover we
544
                        // don't want to punish the introduction node because
545
                        // the blinded failure does not necessarily mean that
7✔
546
                        // the introduction node was at fault.
1✔
547
                        //
6✔
548
                        // TODO(ziggie): Make sure we only keep mc data for
5✔
549
                        // blinded paths, in both the success and failure case,
5✔
550
                        // in memory during the time of the payment and remove
5✔
551
                        // it afterwards. Blinded paths and their blinded hop
5✔
552
                        // keys are always changing per blinded route so there
5✔
553
                        // is no point in persisting this data.
5✔
554
                        i.failBlindedRoute(route)
5✔
555
                }
5✔
556

5✔
557
        // In all other cases, we penalize the reporting node. These are all
5✔
558
        // failures that should not happen.
5✔
559
        default:
5✔
560
                i.failNode(route, errorSourceIdx)
5✔
561
        }
5✔
562
}
5✔
563

5✔
564
// introductionPointIndex returns the index of an introduction point in a
5✔
565
// route, using the same indexing in the route that we use for errorSourceIdx
566
// (i.e., that we consider our own node to be at index zero). A boolean is
567
// returned to indicate whether the route contains a blinded portion at all.
568
func introductionPointIndex(route *mcRoute) (int, bool) {
1✔
569
        for i, hop := range route.hops.Val {
1✔
570
                if hop.hasBlindingPoint.IsSome() {
571
                        return i + 1, true
572
                }
573
        }
574

575
        return 0, false
576
}
577

106✔
578
// processPaymentOutcomeUnknown processes a payment outcome for which no failure
335✔
579
// message or source is available.
244✔
580
func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) {
15✔
581
        n := len(route.hops.Val)
15✔
582

583
        // If this is a direct payment, the destination must be at fault.
584
        if n == 1 {
94✔
585
                i.failNode(route, n)
586
                i.finalFailureReason = &reasonError
587
                return
588
        }
589

1✔
590
        // Otherwise penalize all channels in the route to make sure the
1✔
591
        // responsible node is at least hit too. We even penalize the connection
1✔
592
        // to our own peer, because that peer could also be responsible.
1✔
593
        i.failPairRange(route, 0, n-1)
1✔
UNCOV
594
}
×
UNCOV
595

×
UNCOV
596
// extractMCRoute extracts the fields required by MC from the Route struct to
×
UNCOV
597
// create the more minimal mcRoute struct.
×
598
func extractMCRoute(r *route.Route) *mcRoute {
599
        return &mcRoute{
600
                sourcePubKey: tlv.NewRecordT[tlv.TlvType0](r.SourcePubKey),
601
                totalAmount:  tlv.NewRecordT[tlv.TlvType1](r.TotalAmount),
602
                hops: tlv.NewRecordT[tlv.TlvType2](
1✔
603
                        extractMCHops(r.Hops),
604
                ),
605
        }
606
}
607

80✔
608
// extractMCHops extracts the Hop fields that MC actually uses from a slice of
80✔
609
// Hops.
80✔
610
func extractMCHops(hops []*route.Hop) mcHops {
80✔
611
        return fn.Map(hops, extractMCHop)
80✔
612
}
80✔
613

80✔
614
// extractMCHop extracts the Hop fields that MC actually uses from a Hop.
80✔
615
func extractMCHop(hop *route.Hop) *mcHop {
80✔
616
        h := mcHop{
617
                channelID: tlv.NewPrimitiveRecord[tlv.TlvType0](
618
                        hop.ChannelID,
619
                ),
80✔
620
                pubKeyBytes: tlv.NewRecordT[tlv.TlvType1](hop.PubKeyBytes),
80✔
621
                amtToFwd:    tlv.NewRecordT[tlv.TlvType2](hop.AmtToForward),
80✔
622
        }
623

624
        if hop.BlindingPoint != nil {
176✔
625
                h.hasBlindingPoint = tlv.SomeRecordT(
176✔
626
                        tlv.NewRecordT[tlv.TlvType3](lnwire.TrueBoolean{}),
176✔
627
                )
176✔
628
        }
176✔
629

176✔
630
        if hop.CustomRecords != nil {
176✔
631
                h.hasCustomRecords = tlv.SomeRecordT(
176✔
632
                        tlv.NewRecordT[tlv.TlvType4](lnwire.TrueBoolean{}),
176✔
633
                )
183✔
634
        }
7✔
635

7✔
636
        return &h
7✔
637
}
7✔
638

639
// mcRoute holds the bare minimum info about a payment attempt route that MC
180✔
640
// requires.
4✔
641
type mcRoute struct {
4✔
642
        sourcePubKey tlv.RecordT[tlv.TlvType0, route.Vertex]
4✔
643
        totalAmount  tlv.RecordT[tlv.TlvType1, lnwire.MilliSatoshi]
4✔
644
        hops         tlv.RecordT[tlv.TlvType2, mcHops]
645
}
176✔
646

647
// Record returns a TLV record that can be used to encode/decode an mcRoute
648
// to/from a TLV stream.
649
func (r *mcRoute) Record() tlv.Record {
650
        recordSize := func() uint64 {
651
                var (
652
                        b   bytes.Buffer
653
                        buf [8]byte
654
                )
655
                if err := encodeMCRoute(&b, r, &buf); err != nil {
656
                        panic(err)
657
                }
658

39✔
659
                return uint64(len(b.Bytes()))
55✔
660
        }
16✔
661

16✔
662
        return tlv.MakeDynamicRecord(
16✔
663
                0, r, recordSize, encodeMCRoute, decodeMCRoute,
16✔
664
        )
16✔
UNCOV
665
}
×
666

667
func encodeMCRoute(w io.Writer, val interface{}, _ *[8]byte) error {
668
        if v, ok := val.(*mcRoute); ok {
16✔
669
                return serializeRoute(w, v)
670
        }
671

39✔
672
        return tlv.NewTypeForEncodingErr(val, "routing.mcRoute")
39✔
673
}
39✔
674

675
func decodeMCRoute(r io.Reader, val interface{}, _ *[8]byte, l uint64) error {
676
        if v, ok := val.(*mcRoute); ok {
29✔
677
                route, err := deserializeRoute(io.LimitReader(r, int64(l)))
58✔
678
                if err != nil {
29✔
679
                        return err
29✔
680
                }
UNCOV
681

×
682
                *v = *route
683

684
                return nil
26✔
685
        }
52✔
686

26✔
687
        return tlv.NewTypeForDecodingErr(val, "routing.mcRoute", l, l)
26✔
UNCOV
688
}
×
UNCOV
689

×
690
// mcHops is a list of mcHop records.
691
type mcHops []*mcHop
26✔
692

26✔
693
// Record returns a TLV record that can be used to encode/decode a list of
26✔
694
// mcHop to/from a TLV stream.
695
func (h *mcHops) Record() tlv.Record {
UNCOV
696
        recordSize := func() uint64 {
×
697
                var (
698
                        b   bytes.Buffer
699
                        buf [8]byte
700
                )
701
                if err := encodeMCHops(&b, h, &buf); err != nil {
702
                        panic(err)
703
                }
704

52✔
705
                return uint64(len(b.Bytes()))
81✔
706
        }
29✔
707

29✔
708
        return tlv.MakeDynamicRecord(
29✔
709
                0, h, recordSize, encodeMCHops, decodeMCHops,
29✔
710
        )
29✔
UNCOV
711
}
×
712

713
func encodeMCHops(w io.Writer, val interface{}, buf *[8]byte) error {
714
        if v, ok := val.(*mcHops); ok {
29✔
715
                // Encode the number of hops as a var int.
716
                if err := tlv.WriteVarInt(w, uint64(len(*v)), buf); err != nil {
717
                        return err
52✔
718
                }
52✔
719

52✔
720
                // With that written out, we'll now encode the entries
721
                // themselves as a sub-TLV record, which includes its _own_
722
                // inner length prefix.
55✔
723
                for _, hop := range *v {
110✔
724
                        var hopBytes bytes.Buffer
55✔
725
                        if err := serializeHop(&hopBytes, hop); err != nil {
55✔
726
                                return err
×
727
                        }
×
728

729
                        // We encode the record with a varint length followed by
730
                        // the _raw_ TLV bytes.
731
                        tlvLen := uint64(len(hopBytes.Bytes()))
732
                        if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
126✔
733
                                return err
71✔
734
                        }
71✔
UNCOV
735

×
UNCOV
736
                        if _, err := w.Write(hopBytes.Bytes()); err != nil {
×
737
                                return err
738
                        }
739
                }
740

71✔
741
                return nil
71✔
UNCOV
742
        }
×
UNCOV
743

×
744
        return tlv.NewTypeForEncodingErr(val, "routing.mcHops")
745
}
71✔
UNCOV
746

×
UNCOV
747
func decodeMCHops(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
×
748
        if v, ok := val.(*mcHops); ok {
749
                // First, we'll decode the varint that encodes how many hops
750
                // are encoded in the stream.
55✔
751
                numHops, err := tlv.ReadVarInt(r, buf)
752
                if err != nil {
753
                        return err
×
754
                }
755

756
                // Now that we know how many records we'll need to read, we can
26✔
757
                // iterate and read them all out in series.
52✔
758
                for i := uint64(0); i < numHops; i++ {
26✔
759
                        // Read out the varint that encodes the size of this
26✔
760
                        // inner TLV record.
26✔
761
                        hopSize, err := tlv.ReadVarInt(r, buf)
26✔
UNCOV
762
                        if err != nil {
×
763
                                return err
×
764
                        }
765

766
                        // Using this information, we'll create a new limited
767
                        // reader that'll return an EOF once the end has been
56✔
768
                        // reached so the stream stops consuming bytes.
30✔
769
                        innerTlvReader := &io.LimitedReader{
30✔
770
                                R: r,
30✔
771
                                N: int64(hopSize),
30✔
UNCOV
772
                        }
×
UNCOV
773

×
774
                        hop, err := deserializeHop(innerTlvReader)
775
                        if err != nil {
776
                                return err
777
                        }
778

30✔
779
                        *v = append(*v, hop)
30✔
780
                }
30✔
781

30✔
782
                return nil
30✔
783
        }
30✔
784

30✔
785
        return tlv.NewTypeForDecodingErr(val, "routing.mcHops", l, l)
×
UNCOV
786
}
×
787

788
// mcHop holds the bare minimum info about a payment attempt route hop that MC
30✔
789
// requires.
790
type mcHop struct {
791
        channelID        tlv.RecordT[tlv.TlvType0, uint64]
26✔
792
        pubKeyBytes      tlv.RecordT[tlv.TlvType1, route.Vertex]
793
        amtToFwd         tlv.RecordT[tlv.TlvType2, lnwire.MilliSatoshi]
UNCOV
794
        hasBlindingPoint tlv.OptionalRecordT[tlv.TlvType3, lnwire.TrueBoolean]
×
795
        hasCustomRecords tlv.OptionalRecordT[tlv.TlvType4, lnwire.TrueBoolean]
796
}
797

798
// failNode marks the node indicated by idx in the route as failed. It also
799
// marks the incoming and outgoing channels of the node as failed. This function
800
// intentionally panics when the self node is failed.
801
func (i *interpretedResult) failNode(rt *mcRoute, idx int) {
802
        // Mark the node as failing.
803
        i.nodeFailure = &rt.hops.Val[idx-1].pubKeyBytes.Val
804

805
        // Mark the incoming connection as failed for the node. We intent to
806
        // penalize as much as we can for a node level failure, including future
807
        // outgoing traffic for this connection. The pair as it is returned by
808
        // getPair is penalized in the original and the reversed direction. Note
809
        // that this will also affect the score of the failing node's peers.
810
        // This is necessary to prevent future routes from keep going into the
28✔
811
        // same node again.
28✔
812
        incomingChannelIdx := idx - 1
28✔
813
        inPair, _ := getPair(rt, incomingChannelIdx)
28✔
814
        i.pairResults[inPair] = failPairResult(0)
28✔
815
        i.pairResults[inPair.Reverse()] = failPairResult(0)
28✔
816

28✔
817
        // If not the ultimate node, mark the outgoing connection as failed for
28✔
818
        // the node.
28✔
819
        if idx < len(rt.hops.Val) {
28✔
820
                outgoingChannelIdx := idx
28✔
821
                outPair, _ := getPair(rt, outgoingChannelIdx)
28✔
822
                i.pairResults[outPair] = failPairResult(0)
28✔
823
                i.pairResults[outPair.Reverse()] = failPairResult(0)
28✔
824
        }
28✔
825
}
28✔
826

28✔
827
// failPairRange marks the node pairs from node fromIdx to node toIdx as failed
28✔
828
// in both direction.
50✔
829
func (i *interpretedResult) failPairRange(rt *mcRoute, fromIdx, toIdx int) {
22✔
830
        for idx := fromIdx; idx <= toIdx; idx++ {
22✔
831
                i.failPair(rt, idx)
22✔
832
        }
22✔
833
}
22✔
834

835
// failPair marks a pair as failed in both directions.
836
func (i *interpretedResult) failPair(rt *mcRoute, idx int) {
837
        pair, _ := getPair(rt, idx)
838

2✔
839
        // Report pair in both directions without a minimum penalization amount.
7✔
840
        i.pairResults[pair] = failPairResult(0)
5✔
841
        i.pairResults[pair.Reverse()] = failPairResult(0)
5✔
842
}
843

844
// failPairBalance marks a pair as failed with a minimum penalization amount.
845
func (i *interpretedResult) failPairBalance(rt *mcRoute, channelIdx int) {
12✔
846
        pair, amt := getPair(rt, channelIdx)
12✔
847

12✔
848
        i.pairResults[pair] = failPairResult(amt)
12✔
849
}
12✔
850

12✔
851
// successPairRange marks the node pairs from node fromIdx to node toIdx as
12✔
852
// succeeded.
853
func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) {
854
        for idx := fromIdx; idx <= toIdx; idx++ {
37✔
855
                pair, amt := getPair(rt, idx)
37✔
856

37✔
857
                i.pairResults[pair] = successPairResult(amt)
37✔
858
        }
37✔
859
}
860

861
// failBlindedRoute marks a blinded route as failed for the specific amount to
862
// send by only punishing the last pair.
79✔
863
func (i *interpretedResult) failBlindedRoute(rt *mcRoute) {
192✔
864
        // We fail the last pair of the route, in order to fail the complete
113✔
865
        // blinded route. This is because the combination of ephemeral pubkeys
113✔
866
        // is unique to the route. We fail the last pair in order to not punish
113✔
867
        // the introduction node, since we don't want to disincentivize them
113✔
868
        // from providing that service.
869
        pair, _ := getPair(rt, len(rt.hops.Val)-1)
870

871
        // Since all the hops along a blinded path don't have any amount set, we
872
        // extract the minimal amount to punish from the value that is tried to
5✔
873
        // be sent to the receiver.
5✔
874
        amt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
5✔
875

5✔
876
        i.pairResults[pair] = failPairResult(amt)
5✔
877
}
5✔
878

5✔
879
// markBlindedRouteSuccess marks the hops of the blinded route AFTER the
5✔
880
// introduction node as successful.
5✔
881
//
5✔
882
// NOTE: The introIdx must be the index of the first hop of the blinded route
5✔
883
// AFTER the introduction node.
5✔
884
func (i *interpretedResult) markBlindedRouteSuccess(rt *mcRoute, introIdx int) {
5✔
885
        // For blinded hops we do not have the forwarding amount so we take the
5✔
886
        // minimal amount which went through the route by looking at the last
5✔
887
        // hop.
888
        successAmt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
889
        for idx := introIdx; idx < len(rt.hops.Val); idx++ {
890
                pair, _ := getPair(rt, idx)
891

892
                i.pairResults[pair] = successPairResult(successAmt)
893
        }
4✔
894
}
4✔
895

4✔
896
// getPair returns a node pair from the route and the amount passed between that
4✔
897
// pair.
4✔
898
func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair,
9✔
899
        lnwire.MilliSatoshi) {
5✔
900

5✔
901
        nodeTo := rt.hops.Val[channelIdx].pubKeyBytes.Val
5✔
902
        var (
5✔
903
                nodeFrom route.Vertex
904
                amt      lnwire.MilliSatoshi
905
        )
906

907
        if channelIdx == 0 {
908
                nodeFrom = rt.sourcePubKey.Val
204✔
909
                amt = rt.totalAmount.Val
204✔
910
        } else {
204✔
911
                nodeFrom = rt.hops.Val[channelIdx-1].pubKeyBytes.Val
204✔
912
                amt = rt.hops.Val[channelIdx-1].amtToFwd.Val
204✔
913
        }
204✔
914

204✔
915
        pair := NewDirectedNodePair(nodeFrom, nodeTo)
204✔
916

303✔
917
        return pair, amt
99✔
918
}
99✔
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