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

lightningnetwork / lnd / 13440912774

20 Feb 2025 05:14PM UTC coverage: 57.697% (-1.1%) from 58.802%
13440912774

Pull #9535

github

guggero
GitHub: remove duplicate caching

Turns out that actions/setup-go starting with @v4 also adds caching.
With that, our cache size on disk has almost doubled, leading to the
GitHub runner running out of space in certain situation.
We fix that by disabling the automated caching since we already have our
own, custom-tailored version.
Pull Request #9535: GitHub: remove duplicate caching

103519 of 179417 relevant lines covered (57.7%)

24825.3 hits per line

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

87.48
/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
        if failure.info.IsNone() {
73✔
130
                i.processPaymentOutcomeUnknown(rt)
1✔
131
                return
1✔
132
        }
1✔
133

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

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

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

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

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

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

172
// processPaymentOutcomeBadIntro handles the case where we have made payment
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.
177
func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute,
178
        introIdx, errSourceIdx int) {
2✔
179

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

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

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

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

2✔
203
        switch failure.(type) {
2✔
204

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

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

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

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

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

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

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

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

242
                i.finalFailureReason = &reasonError
5✔
243
        }
244

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

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 {
×
260
                        i.failNode(route, n)
×
261
                        i.finalFailureReason = &reasonError
×
262

×
263
                        return
×
264
                }
×
265

266
                // Otherwise penalize the last pair of the route and retry.
267
                // Either the final node is at fault, or it gets sent a bad htlc
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:
2✔
278

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

2✔
283
                i.finalFailureReason = &reasonIncorrectDetails
2✔
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.
287
        case *lnwire.FailFinalExpiryTooSoon:
×
288
                // TODO(roasbeef): can happen to to race condition, try again
×
289
                // with recent block height
×
290

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

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

300
        // We do not expect to receive an invalid blinding error from the final
301
        // node in the route. This could erroneously happen in the following
302
        // cases:
303
        // 1. Unblinded node: misuses the error code.
304
        // 2. A receiving introduction node: erroneously sends the error code,
305
        //    as the spec indicates that receiving introduction nodes should
306
        //    use regular errors.
307
        //
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:
2✔
313
                failNode()
2✔
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:
3✔
319
                failNode()
3✔
320
        }
321
}
322

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

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

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

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

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

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

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

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

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

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

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

393
        switch failure.(type) {
58✔
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
398
        // outgoing channel set, we will surely hit the responsible node. At
399
        // this point, it is not possible that the node's predecessor corrupted
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.
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:
1✔
415
                reportNode()
1✔
416

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:
2✔
423
                reportOutgoing()
2✔
424

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:
1✔
428

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

1✔
436
                reportOutgoing()
1✔
437

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

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

447
        // When an HTLC parameter is incorrect, the node sending the error may
448
        // be doing something wrong. But it could also be that its predecessor
449
        // is intentionally modifying the htlc parameters that we instructed it
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.
454
        case *lnwire.FailAmountBelowMinimum,
455
                *lnwire.FailFeeInsufficient,
456
                *lnwire.FailIncorrectCltvExpiry:
10✔
457

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

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

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

475
        // If FailExpiryTooSoon is received, there must have been some delay
476
        // along the path. We can't know which node is causing the delay, so we
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:
3✔
482
                reportAll()
3✔
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
491
        // sabotage the blinded route because:
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:
6✔
507
                introIdx, isBlinded := introductionPointIndex(route)
6✔
508

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

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

525
                // If the hop after the introduction node that sent us an
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
531
                // one blinded route per payment.
532
                //
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 {
4✔
537
                        i.finalFailureReason = &reasonError
1✔
538
                } else {
3✔
539
                        // We penalize the final hop of the blinded route which
2✔
540
                        // is sufficient to not reuse this route again and is
2✔
541
                        // also more memory efficient because the other hops
2✔
542
                        // of the blinded path are ephemeral and will only be
2✔
543
                        // used in conjunction with the final hop. Moreover we
2✔
544
                        // don't want to punish the introduction node because
2✔
545
                        // the blinded failure does not necessarily mean that
2✔
546
                        // the introduction node was at fault.
2✔
547
                        //
2✔
548
                        // TODO(ziggie): Make sure we only keep mc data for
2✔
549
                        // blinded paths, in both the success and failure case,
2✔
550
                        // in memory during the time of the payment and remove
2✔
551
                        // it afterwards. Blinded paths and their blinded hop
2✔
552
                        // keys are always changing per blinded route so there
2✔
553
                        // is no point in persisting this data.
2✔
554
                        i.failBlindedRoute(route)
2✔
555
                }
2✔
556

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

564
// introductionPointIndex returns the index of an introduction point in a
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) {
103✔
569
        for i, hop := range route.hops.Val {
329✔
570
                if hop.hasBlindingPoint.IsSome() {
238✔
571
                        return i + 1, true
12✔
572
                }
12✔
573
        }
574

575
        return 0, false
91✔
576
}
577

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

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

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

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

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

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

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

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

636
        return &h
173✔
637
}
638

639
// mcRoute holds the bare minimum info about a payment attempt route that MC
640
// requires.
641
type mcRoute struct {
642
        sourcePubKey tlv.RecordT[tlv.TlvType0, route.Vertex]
643
        totalAmount  tlv.RecordT[tlv.TlvType1, lnwire.MilliSatoshi]
644
        hops         tlv.RecordT[tlv.TlvType2, mcHops]
645
}
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 {
36✔
650
        recordSize := func() uint64 {
49✔
651
                var (
13✔
652
                        b   bytes.Buffer
13✔
653
                        buf [8]byte
13✔
654
                )
13✔
655
                if err := encodeMCRoute(&b, r, &buf); err != nil {
13✔
656
                        panic(err)
×
657
                }
658

659
                return uint64(len(b.Bytes()))
13✔
660
        }
661

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

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

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

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

682
                *v = *route
23✔
683

23✔
684
                return nil
23✔
685
        }
686

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

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

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

705
                return uint64(len(b.Bytes()))
26✔
706
        }
707

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

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

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.
723
                for _, hop := range *v {
120✔
724
                        var hopBytes bytes.Buffer
68✔
725
                        if err := serializeHop(&hopBytes, hop); err != nil {
68✔
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()))
68✔
732
                        if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
68✔
733
                                return err
×
734
                        }
×
735

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

741
                return nil
52✔
742
        }
743

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

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

756
                // Now that we know how many records we'll need to read, we can
757
                // iterate and read them all out in series.
758
                for i := uint64(0); i < numHops; i++ {
50✔
759
                        // Read out the varint that encodes the size of this
27✔
760
                        // inner TLV record.
27✔
761
                        hopSize, err := tlv.ReadVarInt(r, buf)
27✔
762
                        if err != nil {
27✔
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
768
                        // reached so the stream stops consuming bytes.
769
                        innerTlvReader := &io.LimitedReader{
27✔
770
                                R: r,
27✔
771
                                N: int64(hopSize),
27✔
772
                        }
27✔
773

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

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

782
                return nil
23✔
783
        }
784

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

788
// mcHop holds the bare minimum info about a payment attempt route hop that MC
789
// requires.
790
type mcHop struct {
791
        channelID        tlv.RecordT[tlv.TlvType0, uint64]
792
        pubKeyBytes      tlv.RecordT[tlv.TlvType1, route.Vertex]
793
        amtToFwd         tlv.RecordT[tlv.TlvType2, lnwire.MilliSatoshi]
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) {
25✔
802
        // Mark the node as failing.
25✔
803
        i.nodeFailure = &rt.hops.Val[idx-1].pubKeyBytes.Val
25✔
804

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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