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

lightningnetwork / lnd / 13566028875

27 Feb 2025 12:09PM UTC coverage: 49.396% (-9.4%) from 58.748%
13566028875

Pull #9555

github

ellemouton
graph/db: populate the graph cache in Start instead of during construction

In this commit, we move the graph cache population logic out of the
ChannelGraph constructor and into its Start method instead.
Pull Request #9555: graph: extract cache from CRUD [6]

34 of 54 new or added lines in 4 files covered. (62.96%)

27464 existing lines in 436 files now uncovered.

101095 of 204664 relevant lines covered (49.4%)

1.54 hits per line

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

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

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

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

3✔
90
        return fn.ElimOption(failure, func() *interpretedResult {
6✔
91
                i.processSuccess(rt)
3✔
92

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

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

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

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

3✔
120
                return
3✔
121
        }
3✔
122

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

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

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

3✔
139
        failure.info.WhenSome(
3✔
140
                func(r tlv.RecordT[tlv.TlvType0, paymentFailureInfo]) {
6✔
141
                        idx = int(r.Val.sourceIdx.Val)
3✔
142
                        failMsg = r.Val.msg.Val.FailureMessage
3✔
143
                },
3✔
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)
3✔
151
        if isBlinded && introIdx < idx {
3✔
UNCOV
152
                i.processPaymentOutcomeBadIntro(rt, introIdx, idx)
×
UNCOV
153
                return
×
UNCOV
154
        }
×
155

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

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

165
        // An intermediate hop failed. Interpret the outcome, update reputation
166
        // and try again.
167
        default:
3✔
168
                i.processPaymentOutcomeIntermediate(rt, idx, failMsg)
3✔
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,
UNCOV
178
        introIdx, errSourceIdx int) {
×
UNCOV
179

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

×
UNCOV
183
        // Other preceding channels in the route forwarded correctly. Note
×
UNCOV
184
        // that we do not assign success to the incoming link to the
×
UNCOV
185
        // introduction node because it has not handled the error correctly.
×
UNCOV
186
        if introIdx > 1 {
×
UNCOV
187
                i.successPairRange(route, 0, introIdx-2)
×
UNCOV
188
        }
×
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).
UNCOV
194
        if errSourceIdx == len(route.hops.Val) {
×
UNCOV
195
                i.finalFailureReason = &reasonError
×
UNCOV
196
        }
×
197
}
198

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

3✔
203
        switch failure.(type) {
3✔
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,
UNCOV
209
                *lnwire.FailInvalidOnionKey:
×
UNCOV
210

×
UNCOV
211
                i.failNode(rt, 1)
×
UNCOV
212

×
UNCOV
213
                // If this was a payment to a direct peer, we can stop trying.
×
UNCOV
214
                if len(rt.hops.Val) == 1 {
×
UNCOV
215
                        i.finalFailureReason = &reasonError
×
UNCOV
216
                }
×
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:
3✔
223
                log.Warnf("Routing failure for local channel %v occurred",
3✔
224
                        rt.hops.Val[0].channelID)
3✔
225
        }
226
}
227

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

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

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

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

UNCOV
242
                i.finalFailureReason = &reasonError
×
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) {
3✔
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:
3✔
278

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

3✔
283
                i.finalFailureReason = &reasonIncorrectDetails
3✔
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

UNCOV
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)
×
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.
UNCOV
312
        case *lnwire.FailInvalidBlinding:
×
UNCOV
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.
UNCOV
318
        default:
×
UNCOV
319
                failNode()
×
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) {
3✔
329

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

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

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

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

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

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

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

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

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

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

393
        switch failure.(type) {
3✔
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:
3✔
406

3✔
407
                reportOutgoing()
3✔
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.
UNCOV
414
        case *lnwire.InvalidOnionPayload:
×
UNCOV
415
                reportNode()
×
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:
3✔
423
                reportOutgoing()
3✔
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:
3✔
428

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

3✔
436
                reportOutgoing()
3✔
437

3✔
438
                // All nodes up to the failing pair must have forwarded
3✔
439
                // successfully.
3✔
440
                i.successPairRange(route, 0, errorSourceIdx-1)
3✔
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:
3✔
445
                reportOutgoing()
3✔
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:
3✔
457

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

3✔
465
                // We report incoming channel. If a second pair is granted in
3✔
466
                // mission control, this report is ignored.
3✔
467
                reportIncoming()
3✔
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:
3✔
473
                reportOutgoingBalance()
3✔
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.
UNCOV
481
        case *lnwire.FailExpiryTooSoon:
×
UNCOV
482
                reportAll()
×
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:
3✔
507
                introIdx, isBlinded := introductionPointIndex(route)
3✔
508

3✔
509
                // Deal with cases where a node has incorrectly returned a
3✔
510
                // blinding error:
3✔
511
                // 1. A node before the introduction point returned it.
3✔
512
                // 2. A node in a non-blinded route returned it.
3✔
513
                if errorSourceIdx < introIdx || !isBlinded {
3✔
UNCOV
514
                        reportNode()
×
UNCOV
515
                        return
×
UNCOV
516
                }
×
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 {
3✔
UNCOV
537
                        i.finalFailureReason = &reasonError
×
538
                } else {
3✔
539
                        // We penalize the final hop of the blinded route which
3✔
540
                        // is sufficient to not reuse this route again and is
3✔
541
                        // also more memory efficient because the other hops
3✔
542
                        // of the blinded path are ephemeral and will only be
3✔
543
                        // used in conjunction with the final hop. Moreover we
3✔
544
                        // don't want to punish the introduction node because
3✔
545
                        // the blinded failure does not necessarily mean that
3✔
546
                        // the introduction node was at fault.
3✔
547
                        //
3✔
548
                        // TODO(ziggie): Make sure we only keep mc data for
3✔
549
                        // blinded paths, in both the success and failure case,
3✔
550
                        // in memory during the time of the payment and remove
3✔
551
                        // it afterwards. Blinded paths and their blinded hop
3✔
552
                        // keys are always changing per blinded route so there
3✔
553
                        // is no point in persisting this data.
3✔
554
                        i.failBlindedRoute(route)
3✔
555
                }
3✔
556

557
        // In all other cases, we penalize the reporting node. These are all
558
        // failures that should not happen.
UNCOV
559
        default:
×
UNCOV
560
                i.failNode(route, errorSourceIdx)
×
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) {
3✔
569
        for i, hop := range route.hops.Val {
6✔
570
                if hop.hasBlindingPoint.IsSome() {
6✔
571
                        return i + 1, true
3✔
572
                }
3✔
573
        }
574

575
        return 0, false
3✔
576
}
577

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

×
UNCOV
583
        // If this is a direct payment, the destination must be at fault.
×
UNCOV
584
        if n == 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.
UNCOV
593
        i.failPairRange(route, 0, n-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 {
3✔
599
        return &mcRoute{
3✔
600
                sourcePubKey: tlv.NewRecordT[tlv.TlvType0](r.SourcePubKey),
3✔
601
                totalAmount:  tlv.NewRecordT[tlv.TlvType1](r.TotalAmount),
3✔
602
                hops: tlv.NewRecordT[tlv.TlvType2](
3✔
603
                        extractMCHops(r.Hops),
3✔
604
                ),
3✔
605
        }
3✔
606
}
3✔
607

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

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

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

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

636
        return &h
3✔
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 {
3✔
650
        recordSize := func() uint64 {
6✔
651
                var (
3✔
652
                        b   bytes.Buffer
3✔
653
                        buf [8]byte
3✔
654
                )
3✔
655
                if err := encodeMCRoute(&b, r, &buf); err != nil {
3✔
656
                        panic(err)
×
657
                }
658

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

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

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

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

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

682
                *v = *route
3✔
683

3✔
684
                return nil
3✔
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 {
3✔
696
        recordSize := func() uint64 {
6✔
697
                var (
3✔
698
                        b   bytes.Buffer
3✔
699
                        buf [8]byte
3✔
700
                )
3✔
701
                if err := encodeMCHops(&b, h, &buf); err != nil {
3✔
702
                        panic(err)
×
703
                }
704

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

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

713
func encodeMCHops(w io.Writer, val interface{}, buf *[8]byte) error {
3✔
714
        if v, ok := val.(*mcHops); ok {
6✔
715
                // Encode the number of hops as a var int.
3✔
716
                if err := tlv.WriteVarInt(w, uint64(len(*v)), buf); err != nil {
3✔
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 {
6✔
724
                        var hopBytes bytes.Buffer
3✔
725
                        if err := serializeHop(&hopBytes, hop); err != nil {
3✔
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()))
3✔
732
                        if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
3✔
733
                                return err
×
734
                        }
×
735

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

741
                return nil
3✔
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 {
3✔
748
        if v, ok := val.(*mcHops); ok {
6✔
749
                // First, we'll decode the varint that encodes how many hops
3✔
750
                // are encoded in the stream.
3✔
751
                numHops, err := tlv.ReadVarInt(r, buf)
3✔
752
                if err != nil {
3✔
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++ {
6✔
759
                        // Read out the varint that encodes the size of this
3✔
760
                        // inner TLV record.
3✔
761
                        hopSize, err := tlv.ReadVarInt(r, buf)
3✔
762
                        if err != nil {
3✔
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{
3✔
770
                                R: r,
3✔
771
                                N: int64(hopSize),
3✔
772
                        }
3✔
773

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

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

782
                return nil
3✔
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) {
3✔
802
        // Mark the node as failing.
3✔
803
        i.nodeFailure = &rt.hops.Val[idx-1].pubKeyBytes.Val
3✔
804

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

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

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

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

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

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

3✔
848
        i.pairResults[pair] = failPairResult(amt)
3✔
849
}
3✔
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) {
3✔
854
        for idx := fromIdx; idx <= toIdx; idx++ {
6✔
855
                pair, amt := getPair(rt, idx)
3✔
856

3✔
857
                i.pairResults[pair] = successPairResult(amt)
3✔
858
        }
3✔
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) {
3✔
864
        // We fail the last pair of the route, in order to fail the complete
3✔
865
        // blinded route. This is because the combination of ephemeral pubkeys
3✔
866
        // is unique to the route. We fail the last pair in order to not punish
3✔
867
        // the introduction node, since we don't want to disincentivize them
3✔
868
        // from providing that service.
3✔
869
        pair, _ := getPair(rt, len(rt.hops.Val)-1)
3✔
870

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

3✔
876
        i.pairResults[pair] = failPairResult(amt)
3✔
877
}
3✔
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) {
3✔
885
        // For blinded hops we do not have the forwarding amount so we take the
3✔
886
        // minimal amount which went through the route by looking at the last
3✔
887
        // hop.
3✔
888
        successAmt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
3✔
889
        for idx := introIdx; idx < len(rt.hops.Val); idx++ {
6✔
890
                pair, _ := getPair(rt, idx)
3✔
891

3✔
892
                i.pairResults[pair] = successPairResult(successAmt)
3✔
893
        }
3✔
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) {
3✔
900

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

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

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

3✔
917
        return pair, amt
3✔
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