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

lightningnetwork / lnd / 13211764208

08 Feb 2025 03:08AM UTC coverage: 49.288% (-9.5%) from 58.815%
13211764208

Pull #9489

github

calvinrzachman
itest: verify switchrpc server enforces send then track

We prevent the rpc server from allowing onion dispatches for
attempt IDs which have already been tracked by rpc clients.

This helps protect the client from leaking a duplicate onion
attempt. NOTE: This is not the only method for solving this
issue! The issue could be addressed via careful client side
programming which accounts for the uncertainty and async
nature of dispatching onions to a remote process via RPC.
This would require some lnd ChannelRouter changes for how
we intend to use these RPCs though.
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

474 of 990 new or added lines in 11 files covered. (47.88%)

27321 existing lines in 435 files now uncovered.

101192 of 205306 relevant lines covered (49.29%)

1.54 hits per line

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

78.46
/routing/payment_lifecycle.go
1
package routing
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "time"
8

9
        "github.com/btcsuite/btcd/btcec/v2"
10
        "github.com/davecgh/go-spew/spew"
11
        sphinx "github.com/lightningnetwork/lightning-onion"
12
        "github.com/lightningnetwork/lnd/channeldb"
13
        "github.com/lightningnetwork/lnd/fn/v2"
14
        "github.com/lightningnetwork/lnd/graph/db/models"
15
        "github.com/lightningnetwork/lnd/htlcswitch"
16
        "github.com/lightningnetwork/lnd/lntypes"
17
        "github.com/lightningnetwork/lnd/lnwire"
18
        "github.com/lightningnetwork/lnd/routing/route"
19
        "github.com/lightningnetwork/lnd/routing/shards"
20
        "github.com/lightningnetwork/lnd/tlv"
21
)
22

23
// ErrPaymentLifecycleExiting is used when waiting for htlc attempt result, but
24
// the payment lifecycle is exiting .
25
var ErrPaymentLifecycleExiting = errors.New("payment lifecycle exiting")
26

27
// paymentLifecycle holds all information about the current state of a payment
28
// needed to resume if from any point.
29
type paymentLifecycle struct {
30
        router                *ChannelRouter
31
        feeLimit              lnwire.MilliSatoshi
32
        identifier            lntypes.Hash
33
        paySession            PaymentSession
34
        shardTracker          shards.ShardTracker
35
        currentHeight         int32
36
        firstHopCustomRecords lnwire.CustomRecords
37

38
        // quit is closed to signal the sub goroutines of the payment lifecycle
39
        // to stop.
40
        quit chan struct{}
41

42
        // resultCollected is used to signal that the result of an attempt has
43
        // been collected. A nil error means the attempt is either successful
44
        // or failed with temporary error. Otherwise, we should exit the
45
        // lifecycle loop as a terminal error has occurred.
46
        resultCollected chan error
47

48
        // resultCollector is a function that is used to collect the result of
49
        // an HTLC attempt, which is always mounted to `p.collectResultAsync`
50
        // except in unit test, where we use a much simpler resultCollector to
51
        // decouple the test flow for the payment lifecycle.
52
        resultCollector func(attempt *channeldb.HTLCAttempt)
53
}
54

55
// newPaymentLifecycle initiates a new payment lifecycle and returns it.
56
func newPaymentLifecycle(r *ChannelRouter, feeLimit lnwire.MilliSatoshi,
57
        identifier lntypes.Hash, paySession PaymentSession,
58
        shardTracker shards.ShardTracker, currentHeight int32,
59
        firstHopCustomRecords lnwire.CustomRecords) *paymentLifecycle {
3✔
60

3✔
61
        p := &paymentLifecycle{
3✔
62
                router:                r,
3✔
63
                feeLimit:              feeLimit,
3✔
64
                identifier:            identifier,
3✔
65
                paySession:            paySession,
3✔
66
                shardTracker:          shardTracker,
3✔
67
                currentHeight:         currentHeight,
3✔
68
                quit:                  make(chan struct{}),
3✔
69
                resultCollected:       make(chan error, 1),
3✔
70
                firstHopCustomRecords: firstHopCustomRecords,
3✔
71
        }
3✔
72

3✔
73
        // Mount the result collector.
3✔
74
        p.resultCollector = p.collectResultAsync
3✔
75

3✔
76
        return p
3✔
77
}
3✔
78

79
// calcFeeBudget returns the available fee to be used for sending HTLC
80
// attempts.
81
func (p *paymentLifecycle) calcFeeBudget(
82
        feesPaid lnwire.MilliSatoshi) lnwire.MilliSatoshi {
3✔
83

3✔
84
        budget := p.feeLimit
3✔
85

3✔
86
        // We'll subtract the used fee from our fee budget. In case of
3✔
87
        // overflow, we need to check whether feesPaid exceeds our budget
3✔
88
        // already.
3✔
89
        if feesPaid <= budget {
6✔
90
                budget -= feesPaid
3✔
91
        } else {
6✔
92
                budget = 0
3✔
93
        }
3✔
94

95
        return budget
3✔
96
}
97

98
// stateStep defines an action to be taken in our payment lifecycle. We either
99
// quit, continue, or exit the lifecycle, see details below.
100
type stateStep uint8
101

102
const (
103
        // stepSkip is used when we need to skip the current lifecycle and jump
104
        // to the next one.
105
        stepSkip stateStep = iota
106

107
        // stepProceed is used when we can proceed the current lifecycle.
108
        stepProceed
109

110
        // stepExit is used when we need to quit the current lifecycle.
111
        stepExit
112
)
113

114
// decideNextStep is used to determine the next step in the payment lifecycle.
115
func (p *paymentLifecycle) decideNextStep(
116
        payment DBMPPayment) (stateStep, error) {
3✔
117

3✔
118
        // Check whether we could make new HTLC attempts.
3✔
119
        allow, err := payment.AllowMoreAttempts()
3✔
120
        if err != nil {
3✔
UNCOV
121
                return stepExit, err
×
UNCOV
122
        }
×
123

124
        if !allow {
6✔
125
                // Check whether we need to wait for results.
3✔
126
                wait, err := payment.NeedWaitAttempts()
3✔
127
                if err != nil {
3✔
UNCOV
128
                        return stepExit, err
×
UNCOV
129
                }
×
130

131
                // If we are not allowed to make new HTLC attempts and there's
132
                // no need to wait, the lifecycle is done and we can exit.
133
                if !wait {
6✔
134
                        return stepExit, nil
3✔
135
                }
3✔
136

137
                log.Tracef("Waiting for attempt results for payment %v",
3✔
138
                        p.identifier)
3✔
139

3✔
140
                // Otherwise we wait for one HTLC attempt then continue
3✔
141
                // the lifecycle.
3✔
142
                //
3✔
143
                // NOTE: we don't check `p.quit` since `decideNextStep` is
3✔
144
                // running in the same goroutine as `resumePayment`.
3✔
145
                select {
3✔
146
                case err := <-p.resultCollected:
3✔
147
                        // If an error is returned, exit with it.
3✔
148
                        if err != nil {
6✔
149
                                return stepExit, err
3✔
150
                        }
3✔
151

152
                        log.Tracef("Received attempt result for payment %v",
3✔
153
                                p.identifier)
3✔
154

155
                case <-p.router.quit:
3✔
156
                        return stepExit, ErrRouterShuttingDown
3✔
157
                }
158

159
                return stepSkip, nil
3✔
160
        }
161

162
        // Otherwise we need to make more attempts.
163
        return stepProceed, nil
3✔
164
}
165

166
// resumePayment resumes the paymentLifecycle from the current state.
167
func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte,
168
        *route.Route, error) {
3✔
169

3✔
170
        // When the payment lifecycle loop exits, we make sure to signal any
3✔
171
        // sub goroutine of the HTLC attempt to exit, then wait for them to
3✔
172
        // return.
3✔
173
        defer p.stop()
3✔
174

3✔
175
        // If we had any existing attempts outstanding, we'll start by spinning
3✔
176
        // up goroutines that'll collect their results and deliver them to the
3✔
177
        // lifecycle loop below.
3✔
178
        payment, err := p.router.cfg.Control.FetchPayment(p.identifier)
3✔
179
        if err != nil {
3✔
UNCOV
180
                return [32]byte{}, nil, err
×
UNCOV
181
        }
×
182

183
        for _, a := range payment.InFlightHTLCs() {
6✔
184
                a := a
3✔
185

3✔
186
                log.Infof("Resuming HTLC attempt %v for payment %v",
3✔
187
                        a.AttemptID, p.identifier)
3✔
188

3✔
189
                p.resultCollector(&a)
3✔
190
        }
3✔
191

192
        // Get the payment status.
193
        status := payment.GetStatus()
3✔
194

3✔
195
        // exitWithErr is a helper closure that logs and returns an error.
3✔
196
        exitWithErr := func(err error) ([32]byte, *route.Route, error) {
6✔
197
                // Log an error with the latest payment status.
3✔
198
                //
3✔
199
                // NOTE: this `status` variable is reassigned in the loop
3✔
200
                // below. We could also call `payment.GetStatus` here, but in a
3✔
201
                // rare case when the critical log is triggered when using
3✔
202
                // postgres as db backend, the `payment` could be nil, causing
3✔
203
                // the payment fetching to return an error.
3✔
204
                log.Errorf("Payment %v with status=%v failed: %v", p.identifier,
3✔
205
                        status, err)
3✔
206

3✔
207
                return [32]byte{}, nil, err
3✔
208
        }
3✔
209

210
        // We'll continue until either our payment succeeds, or we encounter a
211
        // critical error during path finding.
212
lifecycle:
3✔
213
        for {
6✔
214
                // We update the payment state on every iteration. Since the
3✔
215
                // payment state is affected by multiple goroutines (ie,
3✔
216
                // collectResultAsync), it is NOT guaranteed that we always
3✔
217
                // have the latest state here. This is fine as long as the
3✔
218
                // state is consistent as a whole.
3✔
219
                payment, err = p.router.cfg.Control.FetchPayment(p.identifier)
3✔
220
                if err != nil {
3✔
221
                        return exitWithErr(err)
×
222
                }
×
223

224
                ps := payment.GetState()
3✔
225
                remainingFees := p.calcFeeBudget(ps.FeesPaid)
3✔
226

3✔
227
                status = payment.GetStatus()
3✔
228
                log.Debugf("Payment %v: status=%v, active_shards=%v, "+
3✔
229
                        "rem_value=%v, fee_limit=%v", p.identifier, status,
3✔
230
                        ps.NumAttemptsInFlight, ps.RemainingAmt, remainingFees)
3✔
231

3✔
232
                // We now proceed our lifecycle with the following tasks in
3✔
233
                // order,
3✔
234
                //   1. check context.
3✔
235
                //   2. request route.
3✔
236
                //   3. create HTLC attempt.
3✔
237
                //   4. send HTLC attempt.
3✔
238
                //   5. collect HTLC attempt result.
3✔
239
                //
3✔
240
                // Before we attempt any new shard, we'll check to see if we've
3✔
241
                // gone past the payment attempt timeout, or if the context was
3✔
242
                // cancelled, or the router is exiting. In any of these cases,
3✔
243
                // we'll stop this payment attempt short.
3✔
244
                if err := p.checkContext(ctx); err != nil {
3✔
UNCOV
245
                        return exitWithErr(err)
×
UNCOV
246
                }
×
247

248
                // Now decide the next step of the current lifecycle.
249
                step, err := p.decideNextStep(payment)
3✔
250
                if err != nil {
6✔
251
                        return exitWithErr(err)
3✔
252
                }
3✔
253

254
                switch step {
3✔
255
                // Exit the for loop and return below.
256
                case stepExit:
3✔
257
                        break lifecycle
3✔
258

259
                // Continue the for loop and skip the rest.
260
                case stepSkip:
3✔
261
                        continue lifecycle
3✔
262

263
                // Continue the for loop and proceed the rest.
264
                case stepProceed:
3✔
265

266
                // Unknown step received, exit with an error.
267
                default:
×
268
                        err = fmt.Errorf("unknown step: %v", step)
×
269
                        return exitWithErr(err)
×
270
                }
271

272
                // Now request a route to be used to create our HTLC attempt.
273
                rt, err := p.requestRoute(ps)
3✔
274
                if err != nil {
3✔
UNCOV
275
                        return exitWithErr(err)
×
UNCOV
276
                }
×
277

278
                // We may not be able to find a route for current attempt. In
279
                // that case, we continue the loop and move straight to the
280
                // next iteration in case there are results for inflight HTLCs
281
                // that still need to be collected.
282
                if rt == nil {
6✔
283
                        log.Errorf("No route found for payment %v",
3✔
284
                                p.identifier)
3✔
285

3✔
286
                        continue lifecycle
3✔
287
                }
288

289
                log.Tracef("Found route: %s", spew.Sdump(rt.Hops))
3✔
290

3✔
291
                // Allow the traffic shaper to add custom records to the
3✔
292
                // outgoing HTLC and also adjust the amount if needed.
3✔
293
                err = p.amendFirstHopData(rt)
3✔
294
                if err != nil {
3✔
295
                        return exitWithErr(err)
×
296
                }
×
297

298
                // We found a route to try, create a new HTLC attempt to try.
299
                attempt, err := p.registerAttempt(rt, ps.RemainingAmt)
3✔
300
                if err != nil {
3✔
UNCOV
301
                        return exitWithErr(err)
×
UNCOV
302
                }
×
303

304
                // Once the attempt is created, send it to the htlcswitch.
305
                result, err := p.sendAttempt(attempt)
3✔
306
                if err != nil {
3✔
UNCOV
307
                        return exitWithErr(err)
×
UNCOV
308
                }
×
309

310
                // Now that the shard was successfully sent, launch a go
311
                // routine that will handle its result when its back.
312
                if result.err == nil {
6✔
313
                        p.resultCollector(attempt)
3✔
314
                }
3✔
315
        }
316

317
        // Once we are out the lifecycle loop, it means we've reached a
318
        // terminal condition. We either return the settled preimage or the
319
        // payment's failure reason.
320
        //
321
        // Optionally delete the failed attempts from the database.
322
        err = p.router.cfg.Control.DeleteFailedAttempts(p.identifier)
3✔
323
        if err != nil {
3✔
324
                log.Errorf("Error deleting failed htlc attempts for payment "+
×
325
                        "%v: %v", p.identifier, err)
×
326
        }
×
327

328
        htlc, failure := payment.TerminalInfo()
3✔
329
        if htlc != nil {
6✔
330
                return htlc.Settle.Preimage, &htlc.Route, nil
3✔
331
        }
3✔
332

333
        // Otherwise return the payment failure reason.
334
        return [32]byte{}, nil, *failure
3✔
335
}
336

337
// checkContext checks whether the payment context has been canceled.
338
// Cancellation occurs manually or if the context times out.
339
func (p *paymentLifecycle) checkContext(ctx context.Context) error {
3✔
340
        select {
3✔
341
        case <-ctx.Done():
3✔
342
                // If the context was canceled, we'll mark the payment as
3✔
343
                // failed. There are two cases to distinguish here: Either a
3✔
344
                // user-provided timeout was reached, or the context was
3✔
345
                // canceled, either to a manual cancellation or due to an
3✔
346
                // unknown error.
3✔
347
                var reason channeldb.FailureReason
3✔
348
                if errors.Is(ctx.Err(), context.DeadlineExceeded) {
3✔
UNCOV
349
                        reason = channeldb.FailureReasonTimeout
×
UNCOV
350
                        log.Warnf("Payment attempt not completed before "+
×
UNCOV
351
                                "context timeout, id=%s", p.identifier.String())
×
352
                } else {
3✔
353
                        reason = channeldb.FailureReasonCanceled
3✔
354
                        log.Warnf("Payment attempt context canceled, id=%s",
3✔
355
                                p.identifier.String())
3✔
356
                }
3✔
357

358
                // By marking the payment failed, depending on whether it has
359
                // inflight HTLCs or not, its status will now either be
360
                // `StatusInflight` or `StatusFailed`. In either case, no more
361
                // HTLCs will be attempted.
362
                err := p.router.cfg.Control.FailPayment(p.identifier, reason)
3✔
363
                if err != nil {
3✔
UNCOV
364
                        return fmt.Errorf("FailPayment got %w", err)
×
UNCOV
365
                }
×
366

UNCOV
367
        case <-p.router.quit:
×
UNCOV
368
                return fmt.Errorf("check payment timeout got: %w",
×
UNCOV
369
                        ErrRouterShuttingDown)
×
370

371
        // Fall through if we haven't hit our time limit.
372
        default:
3✔
373
        }
374

375
        return nil
3✔
376
}
377

378
// requestRoute is responsible for finding a route to be used to create an HTLC
379
// attempt.
380
func (p *paymentLifecycle) requestRoute(
381
        ps *channeldb.MPPaymentState) (*route.Route, error) {
3✔
382

3✔
383
        remainingFees := p.calcFeeBudget(ps.FeesPaid)
3✔
384

3✔
385
        // Query our payment session to construct a route.
3✔
386
        rt, err := p.paySession.RequestRoute(
3✔
387
                ps.RemainingAmt, remainingFees,
3✔
388
                uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight),
3✔
389
                p.firstHopCustomRecords,
3✔
390
        )
3✔
391

3✔
392
        // Exit early if there's no error.
3✔
393
        if err == nil {
6✔
394
                return rt, nil
3✔
395
        }
3✔
396

397
        // Otherwise we need to handle the error.
398
        log.Warnf("Failed to find route for payment %v: %v", p.identifier, err)
3✔
399

3✔
400
        // If the error belongs to `noRouteError` set, it means a non-critical
3✔
401
        // error has happened during path finding, and we will mark the payment
3✔
402
        // failed with this reason. Otherwise, we'll return the critical error
3✔
403
        // found to abort the lifecycle.
3✔
404
        var routeErr noRouteError
3✔
405
        if !errors.As(err, &routeErr) {
3✔
UNCOV
406
                return nil, fmt.Errorf("requestRoute got: %w", err)
×
UNCOV
407
        }
×
408

409
        // It's the `paymentSession`'s responsibility to find a route for us
410
        // with the best effort. When it cannot find a path, we need to treat it
411
        // as a terminal condition and fail the payment no matter it has
412
        // inflight HTLCs or not.
413
        failureCode := routeErr.FailureReason()
3✔
414
        log.Warnf("Marking payment %v permanently failed with no route: %v",
3✔
415
                p.identifier, failureCode)
3✔
416

3✔
417
        err = p.router.cfg.Control.FailPayment(p.identifier, failureCode)
3✔
418
        if err != nil {
3✔
UNCOV
419
                return nil, fmt.Errorf("FailPayment got: %w", err)
×
UNCOV
420
        }
×
421

422
        // NOTE: we decide to not return the non-critical noRouteError here to
423
        // avoid terminating the payment lifecycle as there might be other
424
        // inflight HTLCs which we must wait for their results.
425
        return nil, nil
3✔
426
}
427

428
// stop signals any active shard goroutine to exit.
429
func (p *paymentLifecycle) stop() {
3✔
430
        close(p.quit)
3✔
431
}
3✔
432

433
// attemptResult holds the HTLC attempt and a possible error returned from
434
// sending it.
435
type attemptResult struct {
436
        // err is non-nil if a non-critical error was encountered when trying
437
        // to send the attempt, and we successfully updated the control tower
438
        // to reflect this error. This can be errors like not enough local
439
        // balance for the given route etc.
440
        err error
441

442
        // attempt is the attempt structure as recorded in the database.
443
        attempt *channeldb.HTLCAttempt
444
}
445

446
// collectResultAsync launches a goroutine that will wait for the result of the
447
// given HTLC attempt to be available then handle its result. Once received, it
448
// will send a nil error to channel `resultCollected` to indicate there's a
449
// result.
450
func (p *paymentLifecycle) collectResultAsync(attempt *channeldb.HTLCAttempt) {
3✔
451
        log.Debugf("Collecting result for attempt %v in payment %v",
3✔
452
                attempt.AttemptID, p.identifier)
3✔
453

3✔
454
        go func() {
6✔
455
                // Block until the result is available.
3✔
456
                _, err := p.collectResult(attempt)
3✔
457
                if err != nil {
6✔
458
                        log.Errorf("Error collecting result for attempt %v "+
3✔
459
                                "in payment %v: %v", attempt.AttemptID,
3✔
460
                                p.identifier, err)
3✔
461
                }
3✔
462

463
                log.Debugf("Result collected for attempt %v in payment %v",
3✔
464
                        attempt.AttemptID, p.identifier)
3✔
465

3✔
466
                // Once the result is collected, we signal it by writing the
3✔
467
                // error to `resultCollected`.
3✔
468
                select {
3✔
469
                // Send the signal or quit.
470
                case p.resultCollected <- err:
3✔
471

472
                case <-p.quit:
2✔
473
                        log.Debugf("Lifecycle exiting while collecting "+
2✔
474
                                "result for payment %v", p.identifier)
2✔
475

476
                case <-p.router.quit:
2✔
477
                        return
2✔
478
                }
479
        }()
480
}
481

482
// collectResult waits for the result for the given attempt to be available
483
// from the Switch, then records the attempt outcome with the control tower.
484
// An attemptResult is returned, indicating the final outcome of this HTLC
485
// attempt.
486
func (p *paymentLifecycle) collectResult(attempt *channeldb.HTLCAttempt) (
487
        *attemptResult, error) {
3✔
488

3✔
489
        log.Tracef("Collecting result for attempt %v", spew.Sdump(attempt))
3✔
490

3✔
491
        // We'll retrieve the hash specific to this shard from the
3✔
492
        // shardTracker, since it will be needed to regenerate the circuit
3✔
493
        // below.
3✔
494
        hash, err := p.shardTracker.GetHash(attempt.AttemptID)
3✔
495
        if err != nil {
3✔
496
                return p.failAttempt(attempt.AttemptID, err)
×
497
        }
×
498

499
        // Regenerate the circuit for this attempt.
500
        _, circuit, err := GenerateSphinxPacket(
3✔
501
                &attempt.Route, hash[:], attempt.SessionKey(),
3✔
502
        )
3✔
503
        // TODO(yy): We generate this circuit to create the error decryptor,
3✔
504
        // which is then used in htlcswitch as the deobfuscator to decode the
3✔
505
        // error from `UpdateFailHTLC`. However, suppose it's an
3✔
506
        // `UpdateFulfillHTLC` message yet for some reason the sphinx packet is
3✔
507
        // failed to be generated, we'd miss settling it. This means we should
3✔
508
        // give it a second chance to try the settlement path in case
3✔
509
        // `GetAttemptResult` gives us back the preimage. And move the circuit
3✔
510
        // creation into htlcswitch so it's only constructed when there's a
3✔
511
        // failure message we need to decode.
3✔
512
        if err != nil {
3✔
513
                log.Debugf("Unable to generate circuit for attempt %v: %v",
×
514
                        attempt.AttemptID, err)
×
515

×
516
                return p.failAttempt(attempt.AttemptID, err)
×
517
        }
×
518

519
        // Using the created circuit, initialize the error decrypter, so we can
520
        // parse+decode any failures incurred by this payment within the
521
        // switch.
522
        errorDecryptor := &htlcswitch.SphinxErrorDecrypter{
3✔
523
                OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
3✔
524
        }
3✔
525

3✔
526
        // Now ask the switch to return the result of the payment when
3✔
527
        // available.
3✔
528
        //
3✔
529
        // TODO(yy): consider using htlcswitch to create the `errorDecryptor`
3✔
530
        // since the htlc is already in db. This will also make the interface
3✔
531
        // `PaymentAttemptDispatcher` deeper and easier to use. Moreover, we'd
3✔
532
        // only create the decryptor when received a failure, further saving us
3✔
533
        // a few CPU cycles.
3✔
534
        resultChan, err := p.router.cfg.Payer.GetAttemptResult(
3✔
535
                attempt.AttemptID, p.identifier, errorDecryptor,
3✔
536
        )
3✔
537
        // Handle the switch error.
3✔
538
        if err != nil {
3✔
UNCOV
539
                log.Errorf("Failed getting result for attemptID %d "+
×
UNCOV
540
                        "from switch: %v", attempt.AttemptID, err)
×
UNCOV
541

×
UNCOV
542
                return p.handleSwitchErr(attempt, err)
×
UNCOV
543
        }
×
544

545
        // The switch knows about this payment, we'll wait for a result to be
546
        // available.
547
        var (
3✔
548
                result *htlcswitch.PaymentResult
3✔
549
                ok     bool
3✔
550
        )
3✔
551

3✔
552
        select {
3✔
553
        case result, ok = <-resultChan:
3✔
554
                if !ok {
6✔
555
                        return nil, htlcswitch.ErrSwitchExiting
3✔
556
                }
3✔
557

UNCOV
558
        case <-p.quit:
×
UNCOV
559
                return nil, ErrPaymentLifecycleExiting
×
560

UNCOV
561
        case <-p.router.quit:
×
UNCOV
562
                return nil, ErrRouterShuttingDown
×
563
        }
564

565
        // In case of a payment failure, fail the attempt with the control
566
        // tower and return.
567
        if result.Error != nil {
6✔
568
                return p.handleSwitchErr(attempt, result.Error)
3✔
569
        }
3✔
570

571
        // We successfully got a payment result back from the switch.
572
        log.Debugf("Payment %v succeeded with pid=%v",
3✔
573
                p.identifier, attempt.AttemptID)
3✔
574

3✔
575
        // Report success to mission control.
3✔
576
        err = p.router.cfg.MissionControl.ReportPaymentSuccess(
3✔
577
                attempt.AttemptID, &attempt.Route,
3✔
578
        )
3✔
579
        if err != nil {
3✔
580
                log.Errorf("Error reporting payment success to mc: %v", err)
×
581
        }
×
582

583
        // In case of success we atomically store settle result to the DB move
584
        // the shard to the settled state.
585
        htlcAttempt, err := p.router.cfg.Control.SettleAttempt(
3✔
586
                p.identifier, attempt.AttemptID,
3✔
587
                &channeldb.HTLCSettleInfo{
3✔
588
                        Preimage:   result.Preimage,
3✔
589
                        SettleTime: p.router.cfg.Clock.Now(),
3✔
590
                },
3✔
591
        )
3✔
592
        if err != nil {
3✔
UNCOV
593
                log.Errorf("Error settling attempt %v for payment %v with "+
×
UNCOV
594
                        "preimage %v: %v", attempt.AttemptID, p.identifier,
×
UNCOV
595
                        result.Preimage, err)
×
UNCOV
596

×
UNCOV
597
                // We won't mark the attempt as failed since we already have
×
UNCOV
598
                // the preimage.
×
UNCOV
599
                return nil, err
×
UNCOV
600
        }
×
601

602
        return &attemptResult{
3✔
603
                attempt: htlcAttempt,
3✔
604
        }, nil
3✔
605
}
606

607
// registerAttempt is responsible for creating and saving an HTLC attempt in db
608
// by using the route info provided. The `remainingAmt` is used to decide
609
// whether this is the last attempt.
610
func (p *paymentLifecycle) registerAttempt(rt *route.Route,
611
        remainingAmt lnwire.MilliSatoshi) (*channeldb.HTLCAttempt, error) {
3✔
612

3✔
613
        // If this route will consume the last remaining amount to send
3✔
614
        // to the receiver, this will be our last shard (for now).
3✔
615
        isLastAttempt := rt.ReceiverAmt() == remainingAmt
3✔
616

3✔
617
        // Using the route received from the payment session, create a new
3✔
618
        // shard to send.
3✔
619
        attempt, err := p.createNewPaymentAttempt(rt, isLastAttempt)
3✔
620
        if err != nil {
3✔
UNCOV
621
                return nil, err
×
UNCOV
622
        }
×
623

624
        // Before sending this HTLC to the switch, we checkpoint the fresh
625
        // paymentID and route to the DB. This lets us know on startup the ID
626
        // of the payment that we attempted to send, such that we can query the
627
        // Switch for its whereabouts. The route is needed to handle the result
628
        // when it eventually comes back.
629
        err = p.router.cfg.Control.RegisterAttempt(
3✔
630
                p.identifier, &attempt.HTLCAttemptInfo,
3✔
631
        )
3✔
632

3✔
633
        return attempt, err
3✔
634
}
635

636
// createNewPaymentAttempt creates a new payment attempt from the given route.
637
func (p *paymentLifecycle) createNewPaymentAttempt(rt *route.Route,
638
        lastShard bool) (*channeldb.HTLCAttempt, error) {
3✔
639

3✔
640
        // Generate a new key to be used for this attempt.
3✔
641
        sessionKey, err := GenerateNewSessionKey()
3✔
642
        if err != nil {
3✔
643
                return nil, err
×
644
        }
×
645

646
        // We generate a new, unique payment ID that we will use for
647
        // this HTLC.
648
        attemptID, err := p.router.cfg.NextPaymentID()
3✔
649
        if err != nil {
3✔
650
                return nil, err
×
651
        }
×
652

653
        // Request a new shard from the ShardTracker. If this is an AMP
654
        // payment, and this is the last shard, the outstanding shards together
655
        // with this one will be enough for the receiver to derive all HTLC
656
        // preimages. If this a non-AMP payment, the ShardTracker will return a
657
        // simple shard with the payment's static payment hash.
658
        shard, err := p.shardTracker.NewShard(attemptID, lastShard)
3✔
659
        if err != nil {
3✔
UNCOV
660
                return nil, err
×
UNCOV
661
        }
×
662

663
        // If this shard carries MPP or AMP options, add them to the last hop
664
        // on the route.
665
        hop := rt.Hops[len(rt.Hops)-1]
3✔
666
        if shard.MPP() != nil {
6✔
667
                hop.MPP = shard.MPP()
3✔
668
        }
3✔
669

670
        if shard.AMP() != nil {
6✔
671
                hop.AMP = shard.AMP()
3✔
672
        }
3✔
673

674
        hash := shard.Hash()
3✔
675

3✔
676
        // We now have all the information needed to populate the current
3✔
677
        // attempt information.
3✔
678
        attempt := channeldb.NewHtlcAttempt(
3✔
679
                attemptID, sessionKey, *rt, p.router.cfg.Clock.Now(), &hash,
3✔
680
        )
3✔
681

3✔
682
        return attempt, nil
3✔
683
}
684

685
// sendAttempt attempts to send the current attempt to the switch to complete
686
// the payment. If this attempt fails, then we'll continue on to the next
687
// available route.
688
func (p *paymentLifecycle) sendAttempt(
689
        attempt *channeldb.HTLCAttempt) (*attemptResult, error) {
3✔
690

3✔
691
        log.Debugf("Sending HTLC attempt(id=%v, total_amt=%v, first_hop_amt=%d"+
3✔
692
                ") for payment %v", attempt.AttemptID,
3✔
693
                attempt.Route.TotalAmount, attempt.Route.FirstHopAmount.Val,
3✔
694
                p.identifier)
3✔
695

3✔
696
        rt := attempt.Route
3✔
697

3✔
698
        // Construct the first hop.
3✔
699
        firstHop := lnwire.NewShortChanIDFromInt(rt.Hops[0].ChannelID)
3✔
700

3✔
701
        // Craft an HTLC packet to send to the htlcswitch. The metadata within
3✔
702
        // this packet will be used to route the payment through the network,
3✔
703
        // starting with the first-hop.
3✔
704
        htlcAdd := &lnwire.UpdateAddHTLC{
3✔
705
                Amount:        rt.FirstHopAmount.Val.Int(),
3✔
706
                Expiry:        rt.TotalTimeLock,
3✔
707
                PaymentHash:   *attempt.Hash,
3✔
708
                CustomRecords: rt.FirstHopWireCustomRecords,
3✔
709
        }
3✔
710

3✔
711
        // Generate the raw encoded sphinx packet to be included along
3✔
712
        // with the htlcAdd message that we send directly to the
3✔
713
        // switch.
3✔
714
        onionBlob, _, err := GenerateSphinxPacket(
3✔
715
                &rt, attempt.Hash[:], attempt.SessionKey(),
3✔
716
        )
3✔
717
        if err != nil {
3✔
UNCOV
718
                log.Errorf("Failed to create onion blob: attempt=%d in "+
×
UNCOV
719
                        "payment=%v, err:%v", attempt.AttemptID,
×
UNCOV
720
                        p.identifier, err)
×
UNCOV
721

×
UNCOV
722
                return p.failAttempt(attempt.AttemptID, err)
×
UNCOV
723
        }
×
724

725
        copy(htlcAdd.OnionBlob[:], onionBlob)
3✔
726

3✔
727
        // Send it to the Switch. When this method returns we assume
3✔
728
        // the Switch successfully has persisted the payment attempt,
3✔
729
        // such that we can resume waiting for the result after a
3✔
730
        // restart.
3✔
731
        err = p.router.cfg.Payer.SendHTLC(firstHop, attempt.AttemptID, htlcAdd)
3✔
732
        if err != nil {
6✔
733
                log.Errorf("Failed sending attempt %d for payment %v to "+
3✔
734
                        "switch: %v", attempt.AttemptID, p.identifier, err)
3✔
735

3✔
736
                return p.handleSwitchErr(attempt, err)
3✔
737
        }
3✔
738

739
        log.Debugf("Attempt %v for payment %v successfully sent to switch, "+
3✔
740
                "route: %v", attempt.AttemptID, p.identifier, &attempt.Route)
3✔
741

3✔
742
        return &attemptResult{
3✔
743
                attempt: attempt,
3✔
744
        }, nil
3✔
745
}
746

747
// amendFirstHopData is a function that calls the traffic shaper to allow it to
748
// add custom records to the outgoing HTLC and also adjust the amount if
749
// needed.
750
func (p *paymentLifecycle) amendFirstHopData(rt *route.Route) error {
3✔
751
        // The first hop amount on the route is the full route amount if not
3✔
752
        // overwritten by the traffic shaper. So we set the initial value now
3✔
753
        // and potentially overwrite it later.
3✔
754
        rt.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
3✔
755
                tlv.NewBigSizeT(rt.TotalAmount),
3✔
756
        )
3✔
757

3✔
758
        // By default, we set the first hop custom records to the initial
3✔
759
        // value requested by the RPC. The traffic shaper may overwrite this
3✔
760
        // value.
3✔
761
        rt.FirstHopWireCustomRecords = p.firstHopCustomRecords
3✔
762

3✔
763
        // extraDataRequest is a helper struct to pass the custom records and
3✔
764
        // amount back from the traffic shaper.
3✔
765
        type extraDataRequest struct {
3✔
766
                customRecords fn.Option[lnwire.CustomRecords]
3✔
767

3✔
768
                amount fn.Option[lnwire.MilliSatoshi]
3✔
769
        }
3✔
770

3✔
771
        // If a hook exists that may affect our outgoing message, we call it now
3✔
772
        // and apply its side effects to the UpdateAddHTLC message.
3✔
773
        result, err := fn.MapOptionZ(
3✔
774
                p.router.cfg.TrafficShaper,
3✔
775
                //nolint:ll
3✔
776
                func(ts htlcswitch.AuxTrafficShaper) fn.Result[extraDataRequest] {
3✔
UNCOV
777
                        newAmt, newRecords, err := ts.ProduceHtlcExtraData(
×
UNCOV
778
                                rt.TotalAmount, p.firstHopCustomRecords,
×
UNCOV
779
                        )
×
UNCOV
780
                        if err != nil {
×
781
                                return fn.Err[extraDataRequest](err)
×
782
                        }
×
783

784
                        // Make sure we only received valid records.
UNCOV
785
                        if err := newRecords.Validate(); err != nil {
×
786
                                return fn.Err[extraDataRequest](err)
×
787
                        }
×
788

UNCOV
789
                        log.Debugf("Aux traffic shaper returned custom "+
×
UNCOV
790
                                "records %v and amount %d msat for HTLC",
×
UNCOV
791
                                spew.Sdump(newRecords), newAmt)
×
UNCOV
792

×
UNCOV
793
                        return fn.Ok(extraDataRequest{
×
UNCOV
794
                                customRecords: fn.Some(newRecords),
×
UNCOV
795
                                amount:        fn.Some(newAmt),
×
UNCOV
796
                        })
×
797
                },
798
        ).Unpack()
799
        if err != nil {
3✔
800
                return fmt.Errorf("traffic shaper failed to produce extra "+
×
801
                        "data: %w", err)
×
802
        }
×
803

804
        // Apply the side effects to the UpdateAddHTLC message.
805
        result.customRecords.WhenSome(func(records lnwire.CustomRecords) {
3✔
UNCOV
806
                rt.FirstHopWireCustomRecords = records
×
UNCOV
807
        })
×
808
        result.amount.WhenSome(func(amount lnwire.MilliSatoshi) {
3✔
UNCOV
809
                rt.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
×
UNCOV
810
                        tlv.NewBigSizeT(amount),
×
UNCOV
811
                )
×
UNCOV
812
        })
×
813

814
        return nil
3✔
815
}
816

817
// failAttemptAndPayment fails both the payment and its attempt via the
818
// router's control tower, which marks the payment as failed in db.
819
func (p *paymentLifecycle) failPaymentAndAttempt(
820
        attemptID uint64, reason *channeldb.FailureReason,
821
        sendErr error) (*attemptResult, error) {
3✔
822

3✔
823
        log.Errorf("Payment %v failed: final_outcome=%v, raw_err=%v",
3✔
824
                p.identifier, *reason, sendErr)
3✔
825

3✔
826
        // Fail the payment via control tower.
3✔
827
        //
3✔
828
        // NOTE: we must fail the payment first before failing the attempt.
3✔
829
        // Otherwise, once the attempt is marked as failed, another goroutine
3✔
830
        // might make another attempt while we are failing the payment.
3✔
831
        err := p.router.cfg.Control.FailPayment(p.identifier, *reason)
3✔
832
        if err != nil {
3✔
833
                log.Errorf("Unable to fail payment: %v", err)
×
834
                return nil, err
×
835
        }
×
836

837
        // Fail the attempt.
838
        return p.failAttempt(attemptID, sendErr)
3✔
839
}
840

841
// handleSwitchErr inspects the given error from the Switch and determines
842
// whether we should make another payment attempt, or if it should be
843
// considered a terminal error. Terminal errors will be recorded with the
844
// control tower. It analyzes the sendErr for the payment attempt received from
845
// the switch and updates mission control and/or channel policies. Depending on
846
// the error type, the error is either the final outcome of the payment or we
847
// need to continue with an alternative route. A final outcome is indicated by
848
// a non-nil reason value.
849
func (p *paymentLifecycle) handleSwitchErr(attempt *channeldb.HTLCAttempt,
850
        sendErr error) (*attemptResult, error) {
3✔
851

3✔
852
        internalErrorReason := channeldb.FailureReasonError
3✔
853
        attemptID := attempt.AttemptID
3✔
854

3✔
855
        // reportAndFail is a helper closure that reports the failure to the
3✔
856
        // mission control, which helps us to decide whether we want to retry
3✔
857
        // the payment or not. If a non nil reason is returned from mission
3✔
858
        // control, it will further fail the payment via control tower.
3✔
859
        reportAndFail := func(srcIdx *int,
3✔
860
                msg lnwire.FailureMessage) (*attemptResult, error) {
6✔
861

3✔
862
                // Report outcome to mission control.
3✔
863
                reason, err := p.router.cfg.MissionControl.ReportPaymentFail(
3✔
864
                        attemptID, &attempt.Route, srcIdx, msg,
3✔
865
                )
3✔
866
                if err != nil {
3✔
867
                        log.Errorf("Error reporting payment result to mc: %v",
×
868
                                err)
×
869

×
870
                        reason = &internalErrorReason
×
871
                }
×
872

873
                // Fail the attempt only if there's no reason.
874
                if reason == nil {
6✔
875
                        // Fail the attempt.
3✔
876
                        return p.failAttempt(attemptID, sendErr)
3✔
877
                }
3✔
878

879
                // Otherwise fail both the payment and the attempt.
880
                return p.failPaymentAndAttempt(attemptID, reason, sendErr)
3✔
881
        }
882

883
        // If this attempt ID is unknown to the Switch, it means it was never
884
        // checkpointed and forwarded by the switch before a restart. In this
885
        // case we can safely send a new payment attempt, and wait for its
886
        // result to be available.
887
        if errors.Is(sendErr, htlcswitch.ErrPaymentIDNotFound) {
3✔
888
                log.Debugf("Attempt ID %v for payment %v not found in the "+
×
889
                        "Switch, retrying.", attempt.AttemptID, p.identifier)
×
890

×
891
                return p.failAttempt(attemptID, sendErr)
×
892
        }
×
893

894
        if errors.Is(sendErr, htlcswitch.ErrUnreadableFailureMessage) {
3✔
UNCOV
895
                log.Warn("Unreadable failure when sending htlc: id=%v, hash=%v",
×
UNCOV
896
                        attempt.AttemptID, attempt.Hash)
×
UNCOV
897

×
UNCOV
898
                // Since this error message cannot be decrypted, we will send a
×
UNCOV
899
                // nil error message to our mission controller and fail the
×
UNCOV
900
                // payment.
×
UNCOV
901
                return reportAndFail(nil, nil)
×
UNCOV
902
        }
×
903

904
        // If the error is a ClearTextError, we have received a valid wire
905
        // failure message, either from our own outgoing link or from a node
906
        // down the route. If the error is not related to the propagation of
907
        // our payment, we can stop trying because an internal error has
908
        // occurred.
909
        var rtErr htlcswitch.ClearTextError
3✔
910
        ok := errors.As(sendErr, &rtErr)
3✔
911
        if !ok {
3✔
UNCOV
912
                return p.failPaymentAndAttempt(
×
UNCOV
913
                        attemptID, &internalErrorReason, sendErr,
×
UNCOV
914
                )
×
UNCOV
915
        }
×
916

917
        // failureSourceIdx is the index of the node that the failure occurred
918
        // at. If the ClearTextError received is not a ForwardingError the
919
        // payment error occurred at our node, so we leave this value as 0
920
        // to indicate that the failure occurred locally. If the error is a
921
        // ForwardingError, it did not originate at our node, so we set
922
        // failureSourceIdx to the index of the node where the failure occurred.
923
        failureSourceIdx := 0
3✔
924
        var source *htlcswitch.ForwardingError
3✔
925
        ok = errors.As(rtErr, &source)
3✔
926
        if ok {
6✔
927
                failureSourceIdx = source.FailureSourceIdx
3✔
928
        }
3✔
929

930
        // Extract the wire failure and apply channel update if it contains one.
931
        // If we received an unknown failure message from a node along the
932
        // route, the failure message will be nil.
933
        failureMessage := rtErr.WireMessage()
3✔
934
        err := p.handleFailureMessage(
3✔
935
                &attempt.Route, failureSourceIdx, failureMessage,
3✔
936
        )
3✔
937
        if err != nil {
3✔
938
                return p.failPaymentAndAttempt(
×
939
                        attemptID, &internalErrorReason, sendErr,
×
940
                )
×
941
        }
×
942

943
        log.Tracef("Node=%v reported failure when sending htlc",
3✔
944
                failureSourceIdx)
3✔
945

3✔
946
        return reportAndFail(&failureSourceIdx, failureMessage)
3✔
947
}
948

949
// handleFailureMessage tries to apply a channel update present in the failure
950
// message if any.
951
func (p *paymentLifecycle) handleFailureMessage(rt *route.Route,
952
        errorSourceIdx int, failure lnwire.FailureMessage) error {
3✔
953

3✔
954
        if failure == nil {
3✔
UNCOV
955
                return nil
×
UNCOV
956
        }
×
957

958
        // It makes no sense to apply our own channel updates.
959
        if errorSourceIdx == 0 {
6✔
960
                log.Errorf("Channel update of ourselves received")
3✔
961

3✔
962
                return nil
3✔
963
        }
3✔
964

965
        // Extract channel update if the error contains one.
966
        update := p.router.extractChannelUpdate(failure)
3✔
967
        if update == nil {
6✔
968
                return nil
3✔
969
        }
3✔
970

971
        // Parse pubkey to allow validation of the channel update. This should
972
        // always succeed, otherwise there is something wrong in our
973
        // implementation. Therefore, return an error.
974
        errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes
3✔
975
        errSource, err := btcec.ParsePubKey(errVertex[:])
3✔
976
        if err != nil {
3✔
977
                log.Errorf("Cannot parse pubkey: idx=%v, pubkey=%v",
×
978
                        errorSourceIdx, errVertex)
×
979

×
980
                return err
×
981
        }
×
982

983
        var (
3✔
984
                isAdditionalEdge bool
3✔
985
                policy           *models.CachedEdgePolicy
3✔
986
        )
3✔
987

3✔
988
        // Before we apply the channel update, we need to decide whether the
3✔
989
        // update is for additional (ephemeral) edge or normal edge stored in
3✔
990
        // db.
3✔
991
        //
3✔
992
        // Note: the p.paySession might be nil here if it's called inside
3✔
993
        // SendToRoute where there's no payment lifecycle.
3✔
994
        if p.paySession != nil {
6✔
995
                policy = p.paySession.GetAdditionalEdgePolicy(
3✔
996
                        errSource, update.ShortChannelID.ToUint64(),
3✔
997
                )
3✔
998
                if policy != nil {
6✔
999
                        isAdditionalEdge = true
3✔
1000
                }
3✔
1001
        }
1002

1003
        // Apply channel update to additional edge policy.
1004
        if isAdditionalEdge {
6✔
1005
                if !p.paySession.UpdateAdditionalEdge(
3✔
1006
                        update, errSource, policy) {
3✔
1007

×
1008
                        log.Debugf("Invalid channel update received: node=%v",
×
1009
                                errVertex)
×
1010
                }
×
1011
                return nil
3✔
1012
        }
1013

1014
        // Apply channel update to the channel edge policy in our db.
1015
        if !p.router.cfg.ApplyChannelUpdate(update) {
6✔
1016
                log.Debugf("Invalid channel update received: node=%v",
3✔
1017
                        errVertex)
3✔
1018
        }
3✔
1019
        return nil
3✔
1020
}
1021

1022
// failAttempt calls control tower to fail the current payment attempt.
1023
func (p *paymentLifecycle) failAttempt(attemptID uint64,
1024
        sendError error) (*attemptResult, error) {
3✔
1025

3✔
1026
        log.Warnf("Attempt %v for payment %v failed: %v", attemptID,
3✔
1027
                p.identifier, sendError)
3✔
1028

3✔
1029
        failInfo := marshallError(
3✔
1030
                sendError,
3✔
1031
                p.router.cfg.Clock.Now(),
3✔
1032
        )
3✔
1033

3✔
1034
        // Now that we are failing this payment attempt, cancel the shard with
3✔
1035
        // the ShardTracker such that it can derive the correct hash for the
3✔
1036
        // next attempt.
3✔
1037
        if err := p.shardTracker.CancelShard(attemptID); err != nil {
3✔
1038
                return nil, err
×
1039
        }
×
1040

1041
        attempt, err := p.router.cfg.Control.FailAttempt(
3✔
1042
                p.identifier, attemptID, failInfo,
3✔
1043
        )
3✔
1044
        if err != nil {
3✔
UNCOV
1045
                return nil, err
×
UNCOV
1046
        }
×
1047

1048
        return &attemptResult{
3✔
1049
                attempt: attempt,
3✔
1050
                err:     sendError,
3✔
1051
        }, nil
3✔
1052
}
1053

1054
// marshallError marshall an error as received from the switch to a structure
1055
// that is suitable for database storage.
1056
func marshallError(sendError error, time time.Time) *channeldb.HTLCFailInfo {
3✔
1057
        response := &channeldb.HTLCFailInfo{
3✔
1058
                FailTime: time,
3✔
1059
        }
3✔
1060

3✔
1061
        switch {
3✔
1062
        case errors.Is(sendError, htlcswitch.ErrPaymentIDNotFound):
×
1063
                response.Reason = channeldb.HTLCFailInternal
×
1064
                return response
×
1065

UNCOV
1066
        case errors.Is(sendError, htlcswitch.ErrUnreadableFailureMessage):
×
UNCOV
1067
                response.Reason = channeldb.HTLCFailUnreadable
×
UNCOV
1068
                return response
×
1069
        }
1070

1071
        var rtErr htlcswitch.ClearTextError
3✔
1072
        ok := errors.As(sendError, &rtErr)
3✔
1073
        if !ok {
3✔
UNCOV
1074
                response.Reason = channeldb.HTLCFailInternal
×
UNCOV
1075
                return response
×
UNCOV
1076
        }
×
1077

1078
        message := rtErr.WireMessage()
3✔
1079
        if message != nil {
6✔
1080
                response.Reason = channeldb.HTLCFailMessage
3✔
1081
                response.Message = message
3✔
1082
        } else {
3✔
UNCOV
1083
                response.Reason = channeldb.HTLCFailUnknown
×
UNCOV
1084
        }
×
1085

1086
        // If the ClearTextError received is a ForwardingError, the error
1087
        // originated from a node along the route, not locally on our outgoing
1088
        // link. We set failureSourceIdx to the index of the node where the
1089
        // failure occurred. If the error is not a ForwardingError, the failure
1090
        // occurred at our node, so we leave the index as 0 to indicate that
1091
        // we failed locally.
1092
        var fErr *htlcswitch.ForwardingError
3✔
1093
        ok = errors.As(rtErr, &fErr)
3✔
1094
        if ok {
6✔
1095
                response.FailureSourceIndex = uint32(fErr.FailureSourceIdx)
3✔
1096
        }
3✔
1097

1098
        return response
3✔
1099
}
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