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

lightningnetwork / lnd / 20314774459

17 Dec 2025 07:27PM UTC coverage: 65.21%. First build
20314774459

Pull #10049

github

web-flow
Merge 03fa01c8e into 1e15efc47
Pull Request #10049: htlcswitch: add InitAttempt for idempotent external dispatch

228 of 279 new or added lines in 2 files covered. (81.72%)

138021 of 211656 relevant lines covered (65.21%)

20686.12 hits per line

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

83.72
/htlcswitch/payment_result.go
1
package htlcswitch
2

3
import (
4
        "bytes"
5
        "encoding/binary"
6
        "errors"
7
        "fmt"
8
        "io"
9
        "sync"
10

11
        "github.com/lightningnetwork/lnd/channeldb"
12
        "github.com/lightningnetwork/lnd/kvdb"
13
        "github.com/lightningnetwork/lnd/lnwire"
14
        "github.com/lightningnetwork/lnd/multimutex"
15
)
16

17
var (
18

19
        // networkResultStoreBucketKey is used for the root level bucket that
20
        // stores the network result for each payment ID.
21
        networkResultStoreBucketKey = []byte("network-result-store-bucket")
22

23
        // ErrPaymentIDNotFound is an error returned if the given paymentID is
24
        // not found.
25
        ErrPaymentIDNotFound = errors.New("paymentID not found")
26

27
        // ErrPaymentIDAlreadyExists is returned if we try to write a pending
28
        // payment whose paymentID already exists.
29
        ErrPaymentIDAlreadyExists = errors.New("paymentID already exists")
30

31
        // ErrAttemptResultPending is returned if we try to get a result
32
        // for a pending payment whose result is not yet available.
33
        ErrAttemptResultPending = errors.New(
34
                "attempt result not yet available",
35
        )
36

37
        // ErrAmbiguousAttemptInit is returned when any internal error (eg:
38
        // with db read or write) occurs during an InitAttempt call that
39
        // prevents a definitive outcome. This indicates that the state of the
40
        // payment attempt's inititialization is unknown. Callers should retry
41
        // the operation to resolve the ambiguity.
42
        ErrAmbiguousAttemptInit = errors.New(
43
                "ambiguous result for payment attempt registration",
44
        )
45
)
46

47
const (
48
        // pendingHtlcMsgType is a custom message type used to represent a
49
        // pending HTLC in the network result store.
50
        pendingHtlcMsgType lnwire.MessageType = 32768
51
)
52

53
// PaymentResult wraps a decoded result received from the network after a
54
// payment attempt was made. This is what is eventually handed to the router
55
// for processing.
56
type PaymentResult struct {
57
        // Preimage is set by the switch in case a sent HTLC was settled.
58
        Preimage [32]byte
59

60
        // Error is non-nil in case a HTLC send failed, and the HTLC is now
61
        // irrevocably canceled. If the payment failed during forwarding, this
62
        // error will be a *ForwardingError.
63
        Error error
64
}
65

66
// networkResult is the raw result received from the network after a payment
67
// attempt has been made. Since the switch doesn't always have the necessary
68
// data to decode the raw message, we store it together with some meta data,
69
// and decode it when the router query for the final result.
70
type networkResult struct {
71
        // msg is the received result. This should be of type UpdateFulfillHTLC
72
        // or UpdateFailHTLC.
73
        msg lnwire.Message
74

75
        // unencrypted indicates whether the failure encoded in the message is
76
        // unencrypted, and hence doesn't need to be decrypted.
77
        unencrypted bool
78

79
        // isResolution indicates whether this is a resolution message, in
80
        // which the failure reason might not be included.
81
        isResolution bool
82
}
83

84
// serializeNetworkResult serializes the networkResult.
85
func serializeNetworkResult(w io.Writer, n *networkResult) error {
337✔
86
        return channeldb.WriteElements(w, n.msg, n.unencrypted, n.isResolution)
337✔
87
}
337✔
88

89
// deserializeNetworkResult deserializes the networkResult.
90
func deserializeNetworkResult(r io.Reader) (*networkResult, error) {
50✔
91
        n := &networkResult{}
50✔
92

50✔
93
        if err := channeldb.ReadElements(r,
50✔
94
                &n.msg, &n.unencrypted, &n.isResolution,
50✔
95
        ); err != nil {
50✔
96
                return nil, err
×
97
        }
×
98

99
        return n, nil
50✔
100
}
101

102
// networkResultStore is a persistent store that stores any results of HTLCs in
103
// flight on the network. Since payment results are inherently asynchronous, it
104
// is used as a common access point for senders of HTLCs, to know when a result
105
// is back. The Switch will checkpoint any received result to the store, and
106
// the store will keep results and notify the callers about them.
107
type networkResultStore struct {
108
        backend kvdb.Backend
109

110
        // results is a map from paymentIDs to channels where subscribers to
111
        // payment results will be notified.
112
        results    map[uint64][]chan *networkResult
113
        resultsMtx sync.Mutex
114

115
        // attemptIDMtx is a multimutex used to serialize operations for a
116
        // given attempt ID. It ensures InitAttempt's idempotency by protecting
117
        // its read-then-write sequence from concurrent calls, and it maintains
118
        // consistency between the database state and result subscribers.
119
        attemptIDMtx *multimutex.Mutex[uint64]
120
}
121

122
func newNetworkResultStore(db kvdb.Backend) *networkResultStore {
351✔
123
        return &networkResultStore{
351✔
124
                backend:      db,
351✔
125
                results:      make(map[uint64][]chan *networkResult),
351✔
126
                attemptIDMtx: multimutex.NewMutex[uint64](),
351✔
127
        }
351✔
128
}
351✔
129

130
// InitAttempt initializes the payment attempt with the given attemptID.
131
//
132
// If any record (even a pending result placeholder) already exists in the
133
// store, this method returns ErrPaymentIDAlreadyExists. This guarantees that
134
// only one HTLC will be initialized and dispatched for a given attempt ID until
135
// the ID is explicitly cleaned from attempt store.
136
//
137
// If any unexpected internal error occurs (such as a database read or write
138
// failure), it will be wrapped in ErrAmbiguousAttemptInit. This signals
139
// to the caller that the state of the registration is uncertain and that the
140
// operation MUST be retried to resolve the ambiguity.
141
//
142
// NOTE: This is part of the AttemptStore interface. Subscribed clients do not
143
// receive notice of this initialization.
144
func (store *networkResultStore) InitAttempt(attemptID uint64) error {
22✔
145
        // We get a mutex for this attempt ID to serialize init, store, and
22✔
146
        // subscribe operations. This is needed to ensure consistency between
22✔
147
        // the database state and the subscribers in case of concurrent calls.
22✔
148
        store.attemptIDMtx.Lock(attemptID)
22✔
149
        defer store.attemptIDMtx.Unlock(attemptID)
22✔
150

22✔
151
        err := kvdb.Update(store.backend, func(tx kvdb.RwTx) error {
44✔
152
                // Check if any attempt by this ID is already initialized or
22✔
153
                // whether a result for the attempt exists in the store.
22✔
154
                existingResult, err := fetchResult(tx, attemptID)
22✔
155
                if err != nil && !errors.Is(err, ErrPaymentIDNotFound) {
22✔
NEW
156
                        return err
×
NEW
157
                }
×
158

159
                // If the result is already in-progress, return an error
160
                // indicating that the attempt already exists.
161
                if existingResult != nil {
33✔
162
                        log.Warnf("Already initialized attempt for ID=%v",
11✔
163
                                attemptID)
11✔
164

11✔
165
                        return ErrPaymentIDAlreadyExists
11✔
166
                }
11✔
167

168
                // Create an empty networkResult to serve as place holder until
169
                // a result from the network is received.
170
                //
171
                // TODO(calvin): When migrating to native SQL storage, replace
172
                // this custom message placeholder with a proper status enum or
173
                // struct to represent the pending state of an attempt.
174
                pendingMsg, err := lnwire.NewCustom(pendingHtlcMsgType, nil)
11✔
175
                if err != nil {
11✔
NEW
176
                        // This should not happen with a static message type,
×
NEW
177
                        // but if it does, it's an internal error that prevents
×
NEW
178
                        // a definitive outcome, so we must treat it as
×
NEW
179
                        // ambiguous.
×
NEW
180
                        return err
×
NEW
181
                }
×
182
                inProgressResult := &networkResult{
11✔
183
                        msg:          pendingMsg,
11✔
184
                        unencrypted:  true,
11✔
185
                        isResolution: false,
11✔
186
                }
11✔
187

11✔
188
                var b bytes.Buffer
11✔
189
                err = serializeNetworkResult(&b, inProgressResult)
11✔
190
                if err != nil {
11✔
NEW
191
                        return err
×
NEW
192
                }
×
193

194
                var attemptIDBytes [8]byte
11✔
195
                binary.BigEndian.PutUint64(attemptIDBytes[:], attemptID)
11✔
196

11✔
197
                // Mark an attempt with this ID as having been seen by storing
11✔
198
                // the pending placeholder. No network result is available yet,
11✔
199
                // so we do not notify subscribers.
11✔
200
                bucket, err := tx.CreateTopLevelBucket(
11✔
201
                        networkResultStoreBucketKey,
11✔
202
                )
11✔
203
                if err != nil {
11✔
NEW
204
                        return err
×
NEW
205
                }
×
206

207
                return bucket.Put(attemptIDBytes[:], b.Bytes())
11✔
208
        }, func() {
22✔
209
                // No need to reset existingResult here as it's scoped to the
22✔
210
                // transaction.
22✔
211
        })
22✔
212

213
        if err != nil {
33✔
214
                if errors.Is(err, ErrPaymentIDAlreadyExists) {
22✔
215
                        return ErrPaymentIDAlreadyExists
11✔
216
                }
11✔
217

218
                // If any unexpected internal error occurs (such as a database
219
                // failure), it will be wrapped in ErrAmbiguousAttemptInit.
220
                // This signals to the caller that the state of the attempt
221
                // initialization is uncertain and that the operation MUST be
222
                // retried to resolve the ambiguity.
NEW
223
                return fmt.Errorf("%w: %w", ErrAmbiguousAttemptInit, err)
×
224
        }
225

226
        log.Debugf("Initialized attempt for local payment with attemptID=%v",
11✔
227
                attemptID)
11✔
228

11✔
229
        return nil
11✔
230
}
231

232
// StoreResult stores the networkResult for the given attemptID, and notifies
233
// any subscribers.
234
func (store *networkResultStore) StoreResult(attemptID uint64,
235
        result *networkResult) error {
315✔
236

315✔
237
        // We get a mutex for this attempt ID. This is needed to ensure
315✔
238
        // consistency between the database state and the subscribers in case
315✔
239
        // of concurrent calls.
315✔
240
        store.attemptIDMtx.Lock(attemptID)
315✔
241
        defer store.attemptIDMtx.Unlock(attemptID)
315✔
242

315✔
243
        log.Debugf("Storing result for attemptID=%v", attemptID)
315✔
244

315✔
245
        if err := store.storeResult(attemptID, result); err != nil {
315✔
NEW
246
                return err
×
NEW
247
        }
×
248

249
        store.notifySubscribers(attemptID, result)
315✔
250

315✔
251
        return nil
315✔
252
}
253

254
// storeResult persists the given result to the database.
255
func (store *networkResultStore) storeResult(attemptID uint64,
256
        result *networkResult) error {
315✔
257

315✔
258
        var b bytes.Buffer
315✔
259
        if err := serializeNetworkResult(&b, result); err != nil {
315✔
260
                return err
×
261
        }
×
262

263
        var attemptIDBytes [8]byte
315✔
264
        binary.BigEndian.PutUint64(attemptIDBytes[:], attemptID)
315✔
265

315✔
266
        return kvdb.Batch(store.backend, func(tx kvdb.RwTx) error {
630✔
267
                networkResults, err := tx.CreateTopLevelBucket(
315✔
268
                        networkResultStoreBucketKey,
315✔
269
                )
315✔
270
                if err != nil {
315✔
271
                        return err
×
272
                }
×
273

274
                return networkResults.Put(attemptIDBytes[:], b.Bytes())
315✔
275
        })
276
}
277

278
// notifySubscribers notifies any subscribers of the final result for the given
279
// attempt ID.
280
func (store *networkResultStore) notifySubscribers(attemptID uint64,
281
        result *networkResult) {
319✔
282

319✔
283
        store.resultsMtx.Lock()
319✔
284
        for _, res := range store.results[attemptID] {
624✔
285
                res <- result
305✔
286
        }
305✔
287

288
        delete(store.results, attemptID)
319✔
289
        store.resultsMtx.Unlock()
319✔
290
}
291

292
// SubscribeResult is used to get the HTLC attempt result for the given attempt
293
// ID.  It returns a channel on which the result will be delivered when ready.
294
func (store *networkResultStore) SubscribeResult(attemptID uint64) (
295
        <-chan *networkResult, error) {
314✔
296

314✔
297
        // We get a mutex for this payment ID. This is needed to ensure
314✔
298
        // consistency between the database state and the subscribers in case
314✔
299
        // of concurrent calls.
314✔
300
        store.attemptIDMtx.Lock(attemptID)
314✔
301
        defer store.attemptIDMtx.Unlock(attemptID)
314✔
302

314✔
303
        log.Debugf("Subscribing to result for attemptID=%v", attemptID)
314✔
304

314✔
305
        var (
314✔
306
                result     *networkResult
314✔
307
                resultChan = make(chan *networkResult, 1)
314✔
308
        )
314✔
309

314✔
310
        err := kvdb.View(store.backend, func(tx kvdb.RTx) error {
628✔
311
                var err error
314✔
312
                result, err = fetchResult(tx, attemptID)
314✔
313
                switch {
314✔
314

315
                // Result not yet available, we will notify once a result is
316
                // available.
317
                case err == ErrPaymentIDNotFound:
307✔
318
                        return nil
307✔
319

320
                case err != nil:
×
321
                        return err
×
322

323
                // The result was found, and will be returned immediately.
324
                default:
7✔
325
                        return nil
7✔
326
                }
327
        }, func() {
314✔
328
                result = nil
314✔
329
        })
314✔
330
        if err != nil {
314✔
331
                return nil, err
×
332
        }
×
333

334
        // If a result is back from the network, we can send it on the result
335
        // channel immediately. If the result is still our initialized place
336
        // holder, then treat it as not yet available.
337
        if result != nil {
321✔
338
                if result.msg.MsgType() != pendingHtlcMsgType {
12✔
339
                        log.Debugf("Obtained full result for attemptID=%v",
5✔
340
                                attemptID)
5✔
341

5✔
342
                        resultChan <- result
5✔
343

5✔
344
                        return resultChan, nil
5✔
345
                }
5✔
346

347
                log.Debugf("Awaiting result (settle/fail) for attemptID=%v",
2✔
348
                        attemptID)
2✔
349
        }
350

351
        // Otherwise we store the result channel for when the result is
352
        // available.
353
        store.resultsMtx.Lock()
309✔
354
        store.results[attemptID] = append(
309✔
355
                store.results[attemptID], resultChan,
309✔
356
        )
309✔
357
        store.resultsMtx.Unlock()
309✔
358

309✔
359
        return resultChan, nil
309✔
360
}
361

362
// GetResult attempts to immediately fetch the *final* network result for the
363
// given attempt ID from the store.
364
//
365
// NOTE: This method will return ErrAttemptResultPending for attempts
366
// that have been initialized via InitAttempt but for which a final result
367
// (settle/fail) has not yet been stored. ErrPaymentIDNotFound is returned
368
// for attempts that are unknown.
369
func (store *networkResultStore) GetResult(pid uint64) (
370
        *networkResult, error) {
20✔
371

20✔
372
        var result *networkResult
20✔
373
        err := kvdb.View(store.backend, func(tx kvdb.RTx) error {
40✔
374
                var err error
20✔
375
                result, err = fetchResult(tx, pid)
20✔
376
                if err != nil {
29✔
377
                        return err
9✔
378
                }
9✔
379

380
                // If the attempt is still in-flight, treat it as not yet
381
                // available to preserve existing expectation for the behavior
382
                // of this method.
383
                if result.msg.MsgType() == pendingHtlcMsgType {
13✔
384
                        return ErrAttemptResultPending
2✔
385
                }
2✔
386

387
                return nil
9✔
388
        }, func() {
20✔
389
                result = nil
20✔
390
        })
20✔
391
        if err != nil {
31✔
392
                return nil, err
11✔
393
        }
11✔
394

395
        return result, nil
9✔
396
}
397

398
func fetchResult(tx kvdb.RTx, pid uint64) (*networkResult, error) {
360✔
399
        var attemptIDBytes [8]byte
360✔
400
        binary.BigEndian.PutUint64(attemptIDBytes[:], pid)
360✔
401

360✔
402
        networkResults := tx.ReadBucket(networkResultStoreBucketKey)
360✔
403
        if networkResults == nil {
459✔
404
                return nil, ErrPaymentIDNotFound
99✔
405
        }
99✔
406

407
        // Check whether a result is already available.
408
        resultBytes := networkResults.Get(attemptIDBytes[:])
261✔
409
        if resultBytes == nil {
487✔
410
                return nil, ErrPaymentIDNotFound
226✔
411
        }
226✔
412

413
        // Decode the result we found.
414
        r := bytes.NewReader(resultBytes)
35✔
415

35✔
416
        return deserializeNetworkResult(r)
35✔
417
}
418

419
// CleanStore removes all entries from the store, except the payment IDs given.
420
// NOTE: Since every result not listed in the keep map will be deleted, care
421
// should be taken to ensure no new payment attempts are being made
422
// concurrently while this process is ongoing, as its result might end up being
423
// deleted.
424
func (store *networkResultStore) CleanStore(keep map[uint64]struct{}) error {
5✔
425
        return kvdb.Update(store.backend, func(tx kvdb.RwTx) error {
10✔
426
                networkResults, err := tx.CreateTopLevelBucket(
5✔
427
                        networkResultStoreBucketKey,
5✔
428
                )
5✔
429
                if err != nil {
5✔
430
                        return err
×
431
                }
×
432

433
                // Iterate through the bucket, deleting all items not in the
434
                // keep map.
435
                var toClean [][]byte
5✔
436
                if err := networkResults.ForEach(func(k, _ []byte) error {
15✔
437
                        pid := binary.BigEndian.Uint64(k)
10✔
438
                        if _, ok := keep[pid]; ok {
12✔
439
                                return nil
2✔
440
                        }
2✔
441

442
                        toClean = append(toClean, k)
8✔
443
                        return nil
8✔
444
                }); err != nil {
×
445
                        return err
×
446
                }
×
447

448
                for _, k := range toClean {
13✔
449
                        err := networkResults.Delete(k)
8✔
450
                        if err != nil {
8✔
451
                                return err
×
452
                        }
×
453
                }
454

455
                if len(toClean) > 0 {
10✔
456
                        log.Infof("Removed %d stale entries from network "+
5✔
457
                                "result store", len(toClean))
5✔
458
                }
5✔
459

460
                return nil
5✔
461
        }, func() {})
5✔
462
}
463

464
// FetchPendingAttempts returns a list of all attempt IDs that are currently in
465
// the pending state.
466
//
467
// NOTE: This function is NOT safe for concurrent access.
468
func (store *networkResultStore) FetchPendingAttempts() ([]uint64, error) {
218✔
469
        var pending []uint64
218✔
470
        err := kvdb.View(store.backend, func(tx kvdb.RTx) error {
436✔
471
                bucket := tx.ReadBucket(networkResultStoreBucketKey)
218✔
472
                if bucket == nil {
432✔
473
                        return nil
214✔
474
                }
214✔
475

476
                return bucket.ForEach(func(k, v []byte) error {
18✔
477
                        // If the key is not 8 bytes, it's not a valid attempt
11✔
478
                        // ID.
11✔
479
                        if len(k) != 8 {
11✔
NEW
480
                                log.Warnf("Found invalid key of length %d in "+
×
NEW
481
                                        "network result store", len(k))
×
NEW
482

×
NEW
483
                                return nil
×
NEW
484
                        }
×
485

486
                        // Deserialize the result to check its type.
487
                        r := bytes.NewReader(v)
11✔
488
                        result, err := deserializeNetworkResult(r)
11✔
489
                        if err != nil {
11✔
NEW
490
                                // If we can't deserialize, we'll log it and
×
NEW
491
                                // continue. The result will be removed by a
×
NEW
492
                                // call to CleanStore.
×
NEW
493
                                log.Warnf("Unable to deserialize result for "+
×
NEW
494
                                        "key %x: %v", k, err)
×
NEW
495

×
NEW
496
                                return nil
×
NEW
497
                        }
×
498

499
                        // If the result is a pending result, add the attempt
500
                        // ID to our list.
501
                        if result.msg.MsgType() == pendingHtlcMsgType {
18✔
502
                                attemptID := binary.BigEndian.Uint64(k)
7✔
503
                                pending = append(pending, attemptID)
7✔
504
                        }
7✔
505

506
                        return nil
11✔
507
                })
508
        }, func() {
218✔
509
                pending = nil
218✔
510
        })
218✔
511
        if err != nil {
218✔
NEW
512
                return nil, err
×
NEW
513
        }
×
514

515
        return pending, nil
218✔
516
}
517

518
// FailPendingAttempt transitions an initialized attempt from a `pending` to a
519
// `failed` state, recording the provided reason. This ensures that attempts
520
// which fail before being committed to the forwarding engine are properly
521
// finalized. This transition unblocks any subscribers that might be waiting on
522
// a final outcome for an initialized but un-dispatched attempt.
523
//
524
// NOTE: This method is specifically for attempts that have been initialized
525
// via InitAttempt but fail *before* being dispatched to the network. Normal
526
// failures (e.g., from an HTLC being failed on-chain or by a peer) are
527
// recorded via the StoreResult method and should not use FailPendingAttempt.
528
func (store *networkResultStore) FailPendingAttempt(attemptID uint64,
529
        linkErr *LinkError) error {
7✔
530

7✔
531
        // We get a mutex for this attempt ID to ensure consistency between the
7✔
532
        // database state and the subscribers in case of concurrent calls.
7✔
533
        store.attemptIDMtx.Lock(attemptID)
7✔
534
        defer store.attemptIDMtx.Unlock(attemptID)
7✔
535

7✔
536
        // First, create the failure result.
7✔
537
        failureResult, err := newInternalFailureResult(linkErr)
7✔
538
        if err != nil {
7✔
NEW
539
                return fmt.Errorf("failed to create failure message for "+
×
NEW
540
                        "attempt %d: %w", attemptID, err)
×
NEW
541
        }
×
542

543
        var b bytes.Buffer
7✔
544
        if err := serializeNetworkResult(&b, failureResult); err != nil {
7✔
NEW
545
                return fmt.Errorf("failed to serialize failure result: %w", err)
×
NEW
546
        }
×
547
        serializedFailureResult := b.Bytes()
7✔
548

7✔
549
        var attemptIDBytes [8]byte
7✔
550
        binary.BigEndian.PutUint64(attemptIDBytes[:], attemptID)
7✔
551

7✔
552
        err = kvdb.Update(store.backend, func(tx kvdb.RwTx) error {
14✔
553
                // Verify that the attempt exists and is in the pending
7✔
554
                // state, otherwise we should not fail it.
7✔
555
                existingResult, err := fetchResult(tx, attemptID)
7✔
556
                if err != nil {
8✔
557
                        return err
1✔
558
                }
1✔
559

560
                if existingResult.msg.MsgType() != pendingHtlcMsgType {
8✔
561
                        return fmt.Errorf("attempt %d not in pending state",
2✔
562
                                attemptID)
2✔
563
                }
2✔
564

565
                // Write the failure result to the store to unblock any
566
                // subscribers awaiting a final result.
567
                bucket, err := tx.CreateTopLevelBucket(
4✔
568
                        networkResultStoreBucketKey,
4✔
569
                )
4✔
570
                if err != nil {
4✔
NEW
571
                        return err
×
NEW
572
                }
×
573

574
                return bucket.Put(attemptIDBytes[:], serializedFailureResult)
4✔
575
        }, func() {
7✔
576
        })
7✔
577

578
        if err != nil {
10✔
579
                return fmt.Errorf("failed to fail pending attempt %d: %w",
3✔
580
                        attemptID, err)
3✔
581
        }
3✔
582

583
        // Lastly, update any subscribers which may be waiting on the result
584
        // of this attempt.
585
        store.notifySubscribers(attemptID, failureResult)
4✔
586

4✔
587
        return nil
4✔
588
}
589

590
// newInternalFailureResult creates a networkResult representing a terminal,
591
// internally generated failure.
592
func newInternalFailureResult(linkErr *LinkError) (*networkResult, error) {
7✔
593
        // First, we need to serialize the wire message from our link error
7✔
594
        // into a byte slice. This is what the downstream parsers expect.
7✔
595
        var reasonBytes bytes.Buffer
7✔
596
        wireMsg := linkErr.WireMessage()
7✔
597
        if err := lnwire.EncodeFailure(&reasonBytes, wireMsg, 0); err != nil {
7✔
NEW
598
                return nil, err
×
NEW
599
        }
×
600

601
        // We'll create a synthetic UpdateFailHTLC to represent this internal
602
        // failure, following the pattern used by the contract resolver.
603
        failMsg := &lnwire.UpdateFailHTLC{
7✔
604
                Reason: lnwire.OpaqueReason(reasonBytes.Bytes()),
7✔
605
        }
7✔
606

7✔
607
        return &networkResult{
7✔
608
                msg: failMsg,
7✔
609
                // This is a local failure.
7✔
610
                unencrypted: true,
7✔
611
        }, nil
7✔
612
}
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