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

lightningnetwork / lnd / 16181619122

09 Jul 2025 10:33PM UTC coverage: 55.326% (-2.3%) from 57.611%
16181619122

Pull #10060

github

web-flow
Merge d15e8671f into 0e830da9d
Pull Request #10060: sweep: fix expected spending events being missed

9 of 26 new or added lines in 2 files covered. (34.62%)

23695 existing lines in 280 files now uncovered.

108518 of 196143 relevant lines covered (55.33%)

22354.81 hits per line

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

87.64
/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 {
183✔
35
        return pairResult{
183✔
36
                amt: minPenalizeAmt,
183✔
37
        }
183✔
38
}
183✔
39

40
// newSuccessPairResult creates a new result struct for a success.
41
func successPairResult(successAmt lnwire.MilliSatoshi) pairResult {
135✔
42
        return pairResult{
135✔
43
                success: true,
135✔
44
                amt:     successAmt,
135✔
45
        }
135✔
46
}
135✔
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 {
98✔
85

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

98✔
90
        return fn.ElimOption(failure, func() *interpretedResult {
124✔
91
                i.processSuccess(rt)
26✔
92

26✔
93
                return i
26✔
94
        }, func(info paymentFailure) *interpretedResult {
98✔
95
                i.processFail(rt, info)
72✔
96

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

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

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

1✔
120
                return
1✔
121
        }
1✔
122

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

127
// processFail processes a failed payment attempt.
128
func (i *interpretedResult) processFail(rt *mcRoute, failure paymentFailure) {
72✔
129
        // Not having a source index means that we were unable to decrypt the
72✔
130
        // error message.
72✔
131
        if failure.sourceIdx.IsNone() {
73✔
132
                i.processPaymentOutcomeUnknown(rt)
1✔
133
                return
1✔
134
        }
1✔
135

136
        var (
71✔
137
                idx     int
71✔
138
                failMsg lnwire.FailureMessage
71✔
139
        )
71✔
140

71✔
141
        failure.sourceIdx.WhenSome(
71✔
142
                func(r tlv.RecordT[tlv.TlvType0, uint8]) {
142✔
143
                        idx = int(r.Val)
71✔
144

71✔
145
                        failure.msg.WhenSome(
71✔
146
                                func(r tlv.RecordT[tlv.TlvType1,
71✔
147
                                        failureMessage]) {
141✔
148

70✔
149
                                        failMsg = r.Val.FailureMessage
70✔
150
                                },
70✔
151
                        )
152
                },
153
        )
154

155
        // If the payment was to a blinded route and we received an error from
156
        // after the introduction point, handle this error separately - there
157
        // has been a protocol violation from the introduction node. This
158
        // penalty applies regardless of the error code that is returned.
159
        introIdx, isBlinded := introductionPointIndex(rt)
71✔
160
        if isBlinded && introIdx < idx {
73✔
161
                i.processPaymentOutcomeBadIntro(rt, introIdx, idx)
2✔
162
                return
2✔
163
        }
2✔
164

165
        switch idx {
69✔
166
        // We are the source of the failure.
167
        case 0:
2✔
168
                i.processPaymentOutcomeSelf(rt, failMsg)
2✔
169

170
        // A failure from the final hop was received.
171
        case len(rt.hops.Val):
9✔
172
                i.processPaymentOutcomeFinal(rt, failMsg)
9✔
173

174
        // An intermediate hop failed. Interpret the outcome, update reputation
175
        // and try again.
176
        default:
58✔
177
                i.processPaymentOutcomeIntermediate(rt, idx, failMsg)
58✔
178
        }
179
}
180

181
// processPaymentOutcomeBadIntro handles the case where we have made payment
182
// to a blinded route, but received an error from a node after the introduction
183
// node. This indicates that the introduction node is not obeying the route
184
// blinding specification, as we expect all errors from the introduction node
185
// to be source from it.
186
func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute,
187
        introIdx, errSourceIdx int) {
2✔
188

2✔
189
        // We fail the introduction node for not obeying the specification.
2✔
190
        i.failNode(route, introIdx)
2✔
191

2✔
192
        // Other preceding channels in the route forwarded correctly. Note
2✔
193
        // that we do not assign success to the incoming link to the
2✔
194
        // introduction node because it has not handled the error correctly.
2✔
195
        if introIdx > 1 {
3✔
196
                i.successPairRange(route, 0, introIdx-2)
1✔
197
        }
1✔
198

199
        // If the source of the failure was from the final node, we also set
200
        // a final failure reason because the recipient can't process the
201
        // payment (independent of the introduction failing to convert the
202
        // error, we can't complete the payment if the last hop fails).
203
        if errSourceIdx == len(route.hops.Val) {
3✔
204
                i.finalFailureReason = &reasonError
1✔
205
        }
1✔
206
}
207

208
// processPaymentOutcomeSelf handles failures sent by ourselves.
209
func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute,
210
        failure lnwire.FailureMessage) {
2✔
211

2✔
212
        switch failure.(type) {
2✔
213

214
        // We receive a malformed htlc failure from our peer. We trust ourselves
215
        // to send the correct htlc, so our peer must be at fault.
216
        case *lnwire.FailInvalidOnionVersion,
217
                *lnwire.FailInvalidOnionHmac,
218
                *lnwire.FailInvalidOnionKey:
2✔
219

2✔
220
                i.failNode(rt, 1)
2✔
221

2✔
222
                // If this was a payment to a direct peer, we can stop trying.
2✔
223
                if len(rt.hops.Val) == 1 {
3✔
224
                        i.finalFailureReason = &reasonError
1✔
225
                }
1✔
226

227
        // Any other failure originating from ourselves should be temporary and
228
        // caused by changing conditions between path finding and execution of
229
        // the payment. We just retry and trust that the information locally
230
        // available in the link has been updated.
UNCOV
231
        default:
×
UNCOV
232
                log.Warnf("Routing failure for local channel %v occurred",
×
UNCOV
233
                        rt.hops.Val[0].channelID)
×
234
        }
235
}
236

237
// processPaymentOutcomeFinal handles failures sent by the final hop.
238
func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute,
239
        failure lnwire.FailureMessage) {
9✔
240

9✔
241
        n := len(route.hops.Val)
9✔
242

9✔
243
        failNode := func() {
14✔
244
                i.failNode(route, n)
5✔
245

5✔
246
                // Other channels in the route forwarded correctly.
5✔
247
                if n > 1 {
9✔
248
                        i.successPairRange(route, 0, n-2)
4✔
249
                }
4✔
250

251
                i.finalFailureReason = &reasonError
5✔
252
        }
253

254
        // If a failure from the final node is received, we will fail the
255
        // payment in almost all cases. Only when the penultimate node sends an
256
        // incorrect htlc, we want to retry via another route. Invalid onion
257
        // failures are not expected, because the final node wouldn't be able to
258
        // encrypt that failure.
259
        switch failure.(type) {
9✔
260

261
        // Expiry or amount of the HTLC doesn't match the onion, try another
262
        // route.
263
        case *lnwire.FailFinalIncorrectCltvExpiry,
264
                *lnwire.FailFinalIncorrectHtlcAmount:
×
265

×
266
                // We trust ourselves. If this is a direct payment, we penalize
×
267
                // the final node and fail the payment.
×
268
                if n == 1 {
×
269
                        i.failNode(route, n)
×
270
                        i.finalFailureReason = &reasonError
×
271

×
272
                        return
×
273
                }
×
274

275
                // Otherwise penalize the last pair of the route and retry.
276
                // Either the final node is at fault, or it gets sent a bad htlc
277
                // from its predecessor.
278
                i.failPair(route, n-1)
×
279

×
280
                // The other hops relayed correctly, so assign those pairs a
×
281
                // success result. At this point, n >= 2.
×
282
                i.successPairRange(route, 0, n-2)
×
283

284
        // We are using wrong payment hash or amount, fail the payment.
285
        case *lnwire.FailIncorrectPaymentAmount,
286
                *lnwire.FailIncorrectDetails:
2✔
287

2✔
288
                // Assign all pairs a success result, as the payment reached the
2✔
289
                // destination correctly.
2✔
290
                i.successPairRange(route, 0, n-1)
2✔
291

2✔
292
                i.finalFailureReason = &reasonIncorrectDetails
2✔
293

294
        // The HTLC that was extended to the final hop expires too soon. Fail
295
        // the payment, because we may be using the wrong final cltv delta.
296
        case *lnwire.FailFinalExpiryTooSoon:
×
297
                // TODO(roasbeef): can happen to to race condition, try again
×
298
                // with recent block height
×
299

×
300
                // TODO(joostjager): can also happen because a node delayed
×
301
                // deliberately. What to penalize?
×
302
                i.finalFailureReason = &reasonIncorrectDetails
×
303

304
        case *lnwire.FailMPPTimeout:
2✔
305
                // Assign all pairs a success result, as the payment reached the
2✔
306
                // destination correctly. Continue the payment process.
2✔
307
                i.successPairRange(route, 0, n-1)
2✔
308

309
        // We do not expect to receive an invalid blinding error from the final
310
        // node in the route. This could erroneously happen in the following
311
        // cases:
312
        // 1. Unblinded node: misuses the error code.
313
        // 2. A receiving introduction node: erroneously sends the error code,
314
        //    as the spec indicates that receiving introduction nodes should
315
        //    use regular errors.
316
        //
317
        // Note that we expect the case where this error is sent from a node
318
        // after the introduction node to be handled elsewhere as this is part
319
        // of a more general class of errors where the introduction node has
320
        // failed to convert errors for the blinded route.
321
        case *lnwire.FailInvalidBlinding:
2✔
322
                failNode()
2✔
323

324
        // All other errors are considered terminal if coming from the
325
        // final hop. They indicate that something is wrong at the
326
        // recipient, so we do apply a penalty.
327
        default:
3✔
328
                failNode()
3✔
329
        }
330
}
331

332
// processPaymentOutcomeIntermediate handles failures sent by an intermediate
333
// hop.
334
//
335
//nolint:funlen
336
func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
337
        errorSourceIdx int, failure lnwire.FailureMessage) {
58✔
338

58✔
339
        reportOutgoing := func() {
61✔
340
                i.failPair(
3✔
341
                        route, errorSourceIdx,
3✔
342
                )
3✔
343
        }
3✔
344

345
        reportOutgoingBalance := func() {
92✔
346
                i.failPairBalance(
34✔
347
                        route, errorSourceIdx,
34✔
348
                )
34✔
349

34✔
350
                // All nodes up to the failing pair must have forwarded
34✔
351
                // successfully.
34✔
352
                i.successPairRange(route, 0, errorSourceIdx-1)
34✔
353
        }
34✔
354

355
        reportIncoming := func() {
68✔
356
                // We trust ourselves. If the error comes from the first hop, we
10✔
357
                // can penalize the whole node. In that case there is no
10✔
358
                // uncertainty as to which node to blame.
10✔
359
                if errorSourceIdx == 1 {
19✔
360
                        i.failNode(route, errorSourceIdx)
9✔
361
                        return
9✔
362
                }
9✔
363

364
                // Otherwise report the incoming pair.
365
                i.failPair(
1✔
366
                        route, errorSourceIdx-1,
1✔
367
                )
1✔
368

1✔
369
                // All nodes up to the failing pair must have forwarded
1✔
370
                // successfully.
1✔
371
                if errorSourceIdx > 1 {
2✔
372
                        i.successPairRange(route, 0, errorSourceIdx-2)
1✔
373
                }
1✔
374
        }
375

376
        reportNode := func() {
62✔
377
                // Fail only the node that reported the failure.
4✔
378
                i.failNode(route, errorSourceIdx)
4✔
379

4✔
380
                // Other preceding channels in the route forwarded correctly.
4✔
381
                if errorSourceIdx > 1 {
6✔
382
                        i.successPairRange(route, 0, errorSourceIdx-2)
2✔
383
                }
2✔
384
        }
385

386
        reportAll := func() {
61✔
387
                // We trust ourselves. If the error comes from the first hop, we
3✔
388
                // can penalize the whole node. In that case there is no
3✔
389
                // uncertainty as to which node to blame.
3✔
390
                if errorSourceIdx == 1 {
5✔
391
                        i.failNode(route, errorSourceIdx)
2✔
392
                        return
2✔
393
                }
2✔
394

395
                // Otherwise penalize all pairs up to the error source. This
396
                // includes our own outgoing connection.
397
                i.failPairRange(
1✔
398
                        route, 0, errorSourceIdx-1,
1✔
399
                )
1✔
400
        }
401

402
        switch failure.(type) {
58✔
403

404
        // If a node reports onion payload corruption or an invalid version,
405
        // that node may be responsible, but it could also be that it is just
406
        // relaying a malformed htlc failure from it successor. By reporting the
407
        // outgoing channel set, we will surely hit the responsible node. At
408
        // this point, it is not possible that the node's predecessor corrupted
409
        // the onion blob. If the predecessor would have corrupted the payload,
410
        // the error source wouldn't have been able to encrypt this failure
411
        // message for us.
412
        case *lnwire.FailInvalidOnionVersion,
413
                *lnwire.FailInvalidOnionHmac,
UNCOV
414
                *lnwire.FailInvalidOnionKey:
×
UNCOV
415

×
UNCOV
416
                reportOutgoing()
×
417

418
        // If InvalidOnionPayload is received, we penalize only the reporting
419
        // node. We know the preceding hop didn't corrupt the onion, since the
420
        // reporting node is able to send the failure. We assume that we
421
        // constructed a valid onion payload and that the failure is most likely
422
        // an unknown required type or a bug in their implementation.
423
        case *lnwire.InvalidOnionPayload:
1✔
424
                reportNode()
1✔
425

426
        // If the next hop in the route wasn't known or offline, we'll only
427
        // penalize the channel set which we attempted to route over. This is
428
        // conservative, and it can handle faulty channels between nodes
429
        // properly. Additionally, this guards against routing nodes returning
430
        // errors in order to attempt to black list another node.
431
        case *lnwire.FailUnknownNextPeer:
2✔
432
                reportOutgoing()
2✔
433

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

1✔
438
                // Set the node pair for which a channel update may be out of
1✔
439
                // date. The second chance logic uses the policyFailure field.
1✔
440
                i.policyFailure = &DirectedNodePair{
1✔
441
                        From: route.hops.Val[errorSourceIdx-1].pubKeyBytes.Val,
1✔
442
                        To:   route.hops.Val[errorSourceIdx].pubKeyBytes.Val,
1✔
443
                }
1✔
444

1✔
445
                reportOutgoing()
1✔
446

1✔
447
                // All nodes up to the failing pair must have forwarded
1✔
448
                // successfully.
1✔
449
                i.successPairRange(route, 0, errorSourceIdx-1)
1✔
450

451
        // If we get a permanent channel, we'll prune the channel set in both
452
        // directions and continue with the rest of the routes.
UNCOV
453
        case *lnwire.FailPermanentChannelFailure:
×
UNCOV
454
                reportOutgoing()
×
455

456
        // When an HTLC parameter is incorrect, the node sending the error may
457
        // be doing something wrong. But it could also be that its predecessor
458
        // is intentionally modifying the htlc parameters that we instructed it
459
        // via the hop payload. Therefore we penalize the incoming node pair. A
460
        // third cause of this error may be that we have an out of date channel
461
        // update. This is handled by the second chance logic up in mission
462
        // control.
463
        case *lnwire.FailAmountBelowMinimum,
464
                *lnwire.FailFeeInsufficient,
465
                *lnwire.FailIncorrectCltvExpiry:
10✔
466

10✔
467
                // Set the node pair for which a channel update may be out of
10✔
468
                // date. The second chance logic uses the policyFailure field.
10✔
469
                i.policyFailure = &DirectedNodePair{
10✔
470
                        From: route.hops.Val[errorSourceIdx-1].pubKeyBytes.Val,
10✔
471
                        To:   route.hops.Val[errorSourceIdx].pubKeyBytes.Val,
10✔
472
                }
10✔
473

10✔
474
                // We report incoming channel. If a second pair is granted in
10✔
475
                // mission control, this report is ignored.
10✔
476
                reportIncoming()
10✔
477

478
        // If the outgoing channel doesn't have enough capacity, we penalize.
479
        // But we penalize only in a single direction and only for amounts
480
        // greater than the attempted amount.
481
        case *lnwire.FailTemporaryChannelFailure:
34✔
482
                reportOutgoingBalance()
34✔
483

484
        // If FailExpiryTooSoon is received, there must have been some delay
485
        // along the path. We can't know which node is causing the delay, so we
486
        // penalize all of them up to the error source.
487
        //
488
        // Alternatively it could also be that we ourselves have fallen behind
489
        // somehow. We ignore that case for now.
490
        case *lnwire.FailExpiryTooSoon:
3✔
491
                reportAll()
3✔
492

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

6✔
518
                // Deal with cases where a node has incorrectly returned a
6✔
519
                // blinding error:
6✔
520
                // 1. A node before the introduction point returned it.
6✔
521
                // 2. A node in a non-blinded route returned it.
6✔
522
                if errorSourceIdx < introIdx || !isBlinded {
9✔
523
                        reportNode()
3✔
524
                        return
3✔
525
                }
3✔
526

527
                // Otherwise, the error was at the introduction node. All
528
                // nodes up until the introduction node forwarded correctly,
529
                // so we award them as successful.
530
                if introIdx >= 1 {
6✔
531
                        i.successPairRange(route, 0, introIdx-1)
3✔
532
                }
3✔
533

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

566
        // In all other cases, we penalize the reporting node. These are all
567
        // failures that should not happen.
568
        default:
1✔
569
                i.failNode(route, errorSourceIdx)
1✔
570
        }
571
}
572

573
// introductionPointIndex returns the index of an introduction point in a
574
// route, using the same indexing in the route that we use for errorSourceIdx
575
// (i.e., that we consider our own node to be at index zero). A boolean is
576
// returned to indicate whether the route contains a blinded portion at all.
577
func introductionPointIndex(route *mcRoute) (int, bool) {
103✔
578
        for i, hop := range route.hops.Val {
329✔
579
                if hop.hasBlindingPoint.IsSome() {
238✔
580
                        return i + 1, true
12✔
581
                }
12✔
582
        }
583

584
        return 0, false
91✔
585
}
586

587
// processPaymentOutcomeUnknown processes a payment outcome for which no failure
588
// message or source is available.
589
func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) {
1✔
590
        n := len(route.hops.Val)
1✔
591

1✔
592
        // If this is a direct payment, the destination must be at fault.
1✔
593
        if n == 1 {
1✔
594
                i.failNode(route, n)
×
595
                i.finalFailureReason = &reasonError
×
596
                return
×
597
        }
×
598

599
        // Otherwise penalize all channels in the route to make sure the
600
        // responsible node is at least hit too. We even penalize the connection
601
        // to our own peer, because that peer could also be responsible.
602
        i.failPairRange(route, 0, n-1)
1✔
603
}
604

605
// extractMCRoute extracts the fields required by MC from the Route struct to
606
// create the more minimal mcRoute struct.
607
func extractMCRoute(r *route.Route) *mcRoute {
77✔
608
        return &mcRoute{
77✔
609
                sourcePubKey: tlv.NewRecordT[tlv.TlvType0](r.SourcePubKey),
77✔
610
                totalAmount:  tlv.NewRecordT[tlv.TlvType1](r.TotalAmount),
77✔
611
                hops: tlv.NewRecordT[tlv.TlvType2](
77✔
612
                        extractMCHops(r.Hops),
77✔
613
                ),
77✔
614
        }
77✔
615
}
77✔
616

617
// extractMCHops extracts the Hop fields that MC actually uses from a slice of
618
// Hops.
619
func extractMCHops(hops []*route.Hop) mcHops {
77✔
620
        return fn.Map(hops, extractMCHop)
77✔
621
}
77✔
622

623
// extractMCHop extracts the Hop fields that MC actually uses from a Hop.
624
func extractMCHop(hop *route.Hop) *mcHop {
173✔
625
        h := mcHop{
173✔
626
                channelID: tlv.NewPrimitiveRecord[tlv.TlvType0](
173✔
627
                        hop.ChannelID,
173✔
628
                ),
173✔
629
                pubKeyBytes: tlv.NewRecordT[tlv.TlvType1](hop.PubKeyBytes),
173✔
630
                amtToFwd:    tlv.NewRecordT[tlv.TlvType2](hop.AmtToForward),
173✔
631
        }
173✔
632

173✔
633
        if hop.BlindingPoint != nil {
177✔
634
                h.hasBlindingPoint = tlv.SomeRecordT(
4✔
635
                        tlv.NewRecordT[tlv.TlvType3](lnwire.TrueBoolean{}),
4✔
636
                )
4✔
637
        }
4✔
638

639
        if hop.CustomRecords != nil {
174✔
640
                h.hasCustomRecords = tlv.SomeRecordT(
1✔
641
                        tlv.NewRecordT[tlv.TlvType4](lnwire.TrueBoolean{}),
1✔
642
                )
1✔
643
        }
1✔
644

645
        return &h
173✔
646
}
647

648
// mcRoute holds the bare minimum info about a payment attempt route that MC
649
// requires.
650
type mcRoute struct {
651
        sourcePubKey tlv.RecordT[tlv.TlvType0, route.Vertex]
652
        totalAmount  tlv.RecordT[tlv.TlvType1, lnwire.MilliSatoshi]
653
        hops         tlv.RecordT[tlv.TlvType2, mcHops]
654
}
655

656
// Record returns a TLV record that can be used to encode/decode an mcRoute
657
// to/from a TLV stream.
658
func (r *mcRoute) Record() tlv.Record {
36✔
659
        recordSize := func() uint64 {
49✔
660
                var (
13✔
661
                        b   bytes.Buffer
13✔
662
                        buf [8]byte
13✔
663
                )
13✔
664
                if err := encodeMCRoute(&b, r, &buf); err != nil {
13✔
665
                        panic(err)
×
666
                }
667

668
                return uint64(len(b.Bytes()))
13✔
669
        }
670

671
        return tlv.MakeDynamicRecord(
36✔
672
                0, r, recordSize, encodeMCRoute, decodeMCRoute,
36✔
673
        )
36✔
674
}
675

676
func encodeMCRoute(w io.Writer, val interface{}, _ *[8]byte) error {
26✔
677
        if v, ok := val.(*mcRoute); ok {
52✔
678
                return serializeRoute(w, v)
26✔
679
        }
26✔
680

681
        return tlv.NewTypeForEncodingErr(val, "routing.mcRoute")
×
682
}
683

684
func decodeMCRoute(r io.Reader, val interface{}, _ *[8]byte, l uint64) error {
23✔
685
        if v, ok := val.(*mcRoute); ok {
46✔
686
                route, err := deserializeRoute(io.LimitReader(r, int64(l)))
23✔
687
                if err != nil {
23✔
688
                        return err
×
689
                }
×
690

691
                *v = *route
23✔
692

23✔
693
                return nil
23✔
694
        }
695

696
        return tlv.NewTypeForDecodingErr(val, "routing.mcRoute", l, l)
×
697
}
698

699
// mcHops is a list of mcHop records.
700
type mcHops []*mcHop
701

702
// Record returns a TLV record that can be used to encode/decode a list of
703
// mcHop to/from a TLV stream.
704
func (h *mcHops) Record() tlv.Record {
49✔
705
        recordSize := func() uint64 {
75✔
706
                var (
26✔
707
                        b   bytes.Buffer
26✔
708
                        buf [8]byte
26✔
709
                )
26✔
710
                if err := encodeMCHops(&b, h, &buf); err != nil {
26✔
711
                        panic(err)
×
712
                }
713

714
                return uint64(len(b.Bytes()))
26✔
715
        }
716

717
        return tlv.MakeDynamicRecord(
49✔
718
                0, h, recordSize, encodeMCHops, decodeMCHops,
49✔
719
        )
49✔
720
}
721

722
func encodeMCHops(w io.Writer, val interface{}, buf *[8]byte) error {
52✔
723
        if v, ok := val.(*mcHops); ok {
104✔
724
                // Encode the number of hops as a var int.
52✔
725
                if err := tlv.WriteVarInt(w, uint64(len(*v)), buf); err != nil {
52✔
726
                        return err
×
727
                }
×
728

729
                // With that written out, we'll now encode the entries
730
                // themselves as a sub-TLV record, which includes its _own_
731
                // inner length prefix.
732
                for _, hop := range *v {
120✔
733
                        var hopBytes bytes.Buffer
68✔
734
                        if err := serializeHop(&hopBytes, hop); err != nil {
68✔
735
                                return err
×
736
                        }
×
737

738
                        // We encode the record with a varint length followed by
739
                        // the _raw_ TLV bytes.
740
                        tlvLen := uint64(len(hopBytes.Bytes()))
68✔
741
                        if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
68✔
742
                                return err
×
743
                        }
×
744

745
                        if _, err := w.Write(hopBytes.Bytes()); err != nil {
68✔
746
                                return err
×
747
                        }
×
748
                }
749

750
                return nil
52✔
751
        }
752

753
        return tlv.NewTypeForEncodingErr(val, "routing.mcHops")
×
754
}
755

756
func decodeMCHops(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
23✔
757
        if v, ok := val.(*mcHops); ok {
46✔
758
                // First, we'll decode the varint that encodes how many hops
23✔
759
                // are encoded in the stream.
23✔
760
                numHops, err := tlv.ReadVarInt(r, buf)
23✔
761
                if err != nil {
23✔
762
                        return err
×
763
                }
×
764

765
                // Now that we know how many records we'll need to read, we can
766
                // iterate and read them all out in series.
767
                for i := uint64(0); i < numHops; i++ {
50✔
768
                        // Read out the varint that encodes the size of this
27✔
769
                        // inner TLV record.
27✔
770
                        hopSize, err := tlv.ReadVarInt(r, buf)
27✔
771
                        if err != nil {
27✔
772
                                return err
×
773
                        }
×
774

775
                        // Using this information, we'll create a new limited
776
                        // reader that'll return an EOF once the end has been
777
                        // reached so the stream stops consuming bytes.
778
                        innerTlvReader := &io.LimitedReader{
27✔
779
                                R: r,
27✔
780
                                N: int64(hopSize),
27✔
781
                        }
27✔
782

27✔
783
                        hop, err := deserializeHop(innerTlvReader)
27✔
784
                        if err != nil {
27✔
785
                                return err
×
786
                        }
×
787

788
                        *v = append(*v, hop)
27✔
789
                }
790

791
                return nil
23✔
792
        }
793

794
        return tlv.NewTypeForDecodingErr(val, "routing.mcHops", l, l)
×
795
}
796

797
// mcHop holds the bare minimum info about a payment attempt route hop that MC
798
// requires.
799
type mcHop struct {
800
        channelID        tlv.RecordT[tlv.TlvType0, uint64]
801
        pubKeyBytes      tlv.RecordT[tlv.TlvType1, route.Vertex]
802
        amtToFwd         tlv.RecordT[tlv.TlvType2, lnwire.MilliSatoshi]
803
        hasBlindingPoint tlv.OptionalRecordT[tlv.TlvType3, lnwire.TrueBoolean]
804
        hasCustomRecords tlv.OptionalRecordT[tlv.TlvType4, lnwire.TrueBoolean]
805
}
806

807
// failNode marks the node indicated by idx in the route as failed. It also
808
// marks the incoming and outgoing channels of the node as failed. This function
809
// intentionally panics when the self node is failed.
810
func (i *interpretedResult) failNode(rt *mcRoute, idx int) {
25✔
811
        // Mark the node as failing.
25✔
812
        i.nodeFailure = &rt.hops.Val[idx-1].pubKeyBytes.Val
25✔
813

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

25✔
826
        // If not the ultimate node, mark the outgoing connection as failed for
25✔
827
        // the node.
25✔
828
        if idx < len(rt.hops.Val) {
44✔
829
                outgoingChannelIdx := idx
19✔
830
                outPair, _ := getPair(rt, outgoingChannelIdx)
19✔
831
                i.pairResults[outPair] = failPairResult(0)
19✔
832
                i.pairResults[outPair.Reverse()] = failPairResult(0)
19✔
833
        }
19✔
834
}
835

836
// failPairRange marks the node pairs from node fromIdx to node toIdx as failed
837
// in both direction.
838
func (i *interpretedResult) failPairRange(rt *mcRoute, fromIdx, toIdx int) {
2✔
839
        for idx := fromIdx; idx <= toIdx; idx++ {
7✔
840
                i.failPair(rt, idx)
5✔
841
        }
5✔
842
}
843

844
// failPair marks a pair as failed in both directions.
845
func (i *interpretedResult) failPair(rt *mcRoute, idx int) {
9✔
846
        pair, _ := getPair(rt, idx)
9✔
847

9✔
848
        // Report pair in both directions without a minimum penalization amount.
9✔
849
        i.pairResults[pair] = failPairResult(0)
9✔
850
        i.pairResults[pair.Reverse()] = failPairResult(0)
9✔
851
}
9✔
852

853
// failPairBalance marks a pair as failed with a minimum penalization amount.
854
func (i *interpretedResult) failPairBalance(rt *mcRoute, channelIdx int) {
34✔
855
        pair, amt := getPair(rt, channelIdx)
34✔
856

34✔
857
        i.pairResults[pair] = failPairResult(amt)
34✔
858
}
34✔
859

860
// successPairRange marks the node pairs from node fromIdx to node toIdx as
861
// succeeded.
862
func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) {
76✔
863
        for idx := fromIdx; idx <= toIdx; idx++ {
186✔
864
                pair, amt := getPair(rt, idx)
110✔
865

110✔
866
                i.pairResults[pair] = successPairResult(amt)
110✔
867
        }
110✔
868
}
869

870
// failBlindedRoute marks a blinded route as failed for the specific amount to
871
// send by only punishing the last pair.
872
func (i *interpretedResult) failBlindedRoute(rt *mcRoute) {
2✔
873
        // We fail the last pair of the route, in order to fail the complete
2✔
874
        // blinded route. This is because the combination of ephemeral pubkeys
2✔
875
        // is unique to the route. We fail the last pair in order to not punish
2✔
876
        // the introduction node, since we don't want to disincentivize them
2✔
877
        // from providing that service.
2✔
878
        pair, _ := getPair(rt, len(rt.hops.Val)-1)
2✔
879

2✔
880
        // Since all the hops along a blinded path don't have any amount set, we
2✔
881
        // extract the minimal amount to punish from the value that is tried to
2✔
882
        // be sent to the receiver.
2✔
883
        amt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
2✔
884

2✔
885
        i.pairResults[pair] = failPairResult(amt)
2✔
886
}
2✔
887

888
// markBlindedRouteSuccess marks the hops of the blinded route AFTER the
889
// introduction node as successful.
890
//
891
// NOTE: The introIdx must be the index of the first hop of the blinded route
892
// AFTER the introduction node.
893
func (i *interpretedResult) markBlindedRouteSuccess(rt *mcRoute, introIdx int) {
1✔
894
        // For blinded hops we do not have the forwarding amount so we take the
1✔
895
        // minimal amount which went through the route by looking at the last
1✔
896
        // hop.
1✔
897
        successAmt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
1✔
898
        for idx := introIdx; idx < len(rt.hops.Val); idx++ {
3✔
899
                pair, _ := getPair(rt, idx)
2✔
900

2✔
901
                i.pairResults[pair] = successPairResult(successAmt)
2✔
902
        }
2✔
903
}
904

905
// getPair returns a node pair from the route and the amount passed between that
906
// pair.
907
func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair,
908
        lnwire.MilliSatoshi) {
201✔
909

201✔
910
        nodeTo := rt.hops.Val[channelIdx].pubKeyBytes.Val
201✔
911
        var (
201✔
912
                nodeFrom route.Vertex
201✔
913
                amt      lnwire.MilliSatoshi
201✔
914
        )
201✔
915

201✔
916
        if channelIdx == 0 {
297✔
917
                nodeFrom = rt.sourcePubKey.Val
96✔
918
                amt = rt.totalAmount.Val
96✔
919
        } else {
201✔
920
                nodeFrom = rt.hops.Val[channelIdx-1].pubKeyBytes.Val
105✔
921
                amt = rt.hops.Val[channelIdx-1].amtToFwd.Val
105✔
922
        }
105✔
923

924
        pair := NewDirectedNodePair(nodeFrom, nodeTo)
201✔
925

201✔
926
        return pair, amt
201✔
927
}
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