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

lightningnetwork / lnd / 13051234467

30 Jan 2025 11:19AM UTC coverage: 49.289% (-9.5%) from 58.782%
13051234467

Pull #9459

github

ziggie1984
docs: add release-notes.
Pull Request #9459: invoices: amp invoices bugfix.

27 of 54 new or added lines in 4 files covered. (50.0%)

27265 existing lines in 434 files now uncovered.

100654 of 204212 relevant lines covered (49.29%)

1.54 hits per line

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

63.72
/invoices/update_invoice.go
1
package invoices
2

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

8
        "github.com/lightningnetwork/lnd/graph/db/models"
9
        "github.com/lightningnetwork/lnd/lntypes"
10
        "github.com/lightningnetwork/lnd/lnwire"
11
)
12

13
// updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its
14
// set ID), and updates the internal AMP state of an invoice, and also tallies
15
// the set of HTLCs to be updated on disk.
16
func acceptHtlcsAmp(invoice *Invoice, setID SetID,
17
        circuitKey models.CircuitKey, htlc *InvoiceHTLC,
18
        updater InvoiceUpdater) error {
3✔
19

3✔
20
        newAmpState, err := getUpdatedInvoiceAmpState(
3✔
21
                invoice, setID, circuitKey, HtlcStateAccepted, htlc.Amt,
3✔
22
        )
3✔
23
        if err != nil {
3✔
24
                return err
×
25
        }
×
26

27
        invoice.AMPState[setID] = newAmpState
3✔
28

3✔
29
        // Mark the updates as needing to be written to disk.
3✔
30
        return updater.UpdateAmpState(setID, newAmpState, circuitKey)
3✔
31
}
32

33
// cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP
34
// HTLC set. We'll need to update the meta data in the  main invoice, and also
35
// apply the new update to the update MAP, since all the HTLCs for a given HTLC
36
// set need to be written in-line with each other.
37
func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
UNCOV
38
        htlc *InvoiceHTLC, updater InvoiceUpdater) error {
×
UNCOV
39

×
UNCOV
40
        setID := htlc.AMP.Record.SetID()
×
UNCOV
41

×
UNCOV
42
        // First, we'll update the state of the entire HTLC set
×
UNCOV
43
        // to cancelled.
×
UNCOV
44
        newAmpState, err := getUpdatedInvoiceAmpState(
×
UNCOV
45
                invoice, setID, circuitKey, HtlcStateCanceled,
×
UNCOV
46
                htlc.Amt,
×
UNCOV
47
        )
×
UNCOV
48
        if err != nil {
×
49
                return err
×
50
        }
×
51

UNCOV
52
        invoice.AMPState[setID] = newAmpState
×
UNCOV
53

×
UNCOV
54
        // Mark the updates as needing to be written to disk.
×
UNCOV
55
        err = updater.UpdateAmpState(setID, newAmpState, circuitKey)
×
UNCOV
56
        if err != nil {
×
57
                return err
×
58
        }
×
59

60
        // We'll only decrement the total amount paid if the invoice was
61
        // already in the accepted state.
UNCOV
62
        if invoice.AmtPaid != 0 {
×
UNCOV
63
                return updateInvoiceAmtPaid(
×
UNCOV
64
                        invoice, invoice.AmtPaid-htlc.Amt, updater,
×
UNCOV
65
                )
×
UNCOV
66
        }
×
67

68
        return nil
×
69
}
70

71
// settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP
72
// invoice. We'll update some meta data in the main invoice, and also signal
73
// that this HTLC set needs to be re-written back to disk.
74
func settleHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
75
        htlc *InvoiceHTLC, updater InvoiceUpdater) error {
3✔
76

3✔
77
        setID := htlc.AMP.Record.SetID()
3✔
78

3✔
79
        // Next update the main AMP meta-data to indicate that this HTLC set
3✔
80
        // has been fully settled.
3✔
81
        newAmpState, err := getUpdatedInvoiceAmpState(
3✔
82
                invoice, setID, circuitKey, HtlcStateSettled, 0,
3✔
83
        )
3✔
84
        if err != nil {
3✔
85
                return err
×
86
        }
×
87

88
        invoice.AMPState[setID] = newAmpState
3✔
89

3✔
90
        // Mark the updates as needing to be written to disk.
3✔
91
        return updater.UpdateAmpState(setID, newAmpState, circuitKey)
3✔
92
}
93

94
// UpdateInvoice fetches the invoice, obtains the update descriptor from the
95
// callback and applies the updates in a single db transaction.
96
func UpdateInvoice(hash *lntypes.Hash, invoice *Invoice,
97
        updateTime time.Time, callback InvoiceUpdateCallback,
98
        updater InvoiceUpdater) (*Invoice, error) {
3✔
99

3✔
100
        // Create deep copy to prevent any accidental modification in the
3✔
101
        // callback.
3✔
102
        invoiceCopy, err := CopyInvoice(invoice)
3✔
103
        if err != nil {
3✔
104
                return nil, err
×
105
        }
×
106

107
        // Call the callback and obtain the update descriptor.
108
        update, err := callback(invoiceCopy)
3✔
109
        if err != nil {
3✔
UNCOV
110
                return invoice, err
×
UNCOV
111
        }
×
112

113
        // If there is nothing to update, return early.
114
        if update == nil {
6✔
115
                return invoice, nil
3✔
116
        }
3✔
117

118
        switch update.UpdateType {
3✔
119
        case CancelHTLCsUpdate:
3✔
120
                err := cancelHTLCs(invoice, updateTime, update, updater)
3✔
121
                if err != nil {
3✔
122
                        return nil, err
×
123
                }
×
124

125
        case AddHTLCsUpdate:
3✔
126
                err := addHTLCs(invoice, hash, updateTime, update, updater)
3✔
127
                if err != nil {
3✔
UNCOV
128
                        return nil, err
×
UNCOV
129
                }
×
130

131
        case SettleHodlInvoiceUpdate:
3✔
132
                err := settleHodlInvoice(
3✔
133
                        invoice, hash, updateTime, update.State, updater,
3✔
134
                )
3✔
135
                if err != nil {
3✔
136
                        return nil, err
×
137
                }
×
138

139
        case CancelInvoiceUpdate:
3✔
140
                err := cancelInvoice(
3✔
141
                        invoice, hash, updateTime, update.State, updater,
3✔
142
                )
3✔
143
                if err != nil {
6✔
144
                        return nil, err
3✔
145
                }
3✔
146

147
        default:
×
148
                return nil, fmt.Errorf("unknown update type: %s",
×
149
                        update.UpdateType)
×
150
        }
151

152
        if err := updater.Finalize(update.UpdateType); err != nil {
3✔
UNCOV
153
                return nil, err
×
UNCOV
154
        }
×
155

156
        return invoice, nil
3✔
157
}
158

159
// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc.
160
//
161
// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the
162
// InvoiceUpdateDesc.
163
func cancelHTLCs(invoice *Invoice, updateTime time.Time,
164
        update *InvoiceUpdateDesc, updater InvoiceUpdater) error {
3✔
165

3✔
166
        for key := range update.CancelHtlcs {
3✔
UNCOV
167
                htlc, exists := invoice.Htlcs[key]
×
UNCOV
168

×
UNCOV
169
                // Verify that we don't get an action for htlcs that are not
×
UNCOV
170
                // present on the invoice.
×
UNCOV
171
                if !exists {
×
172
                        return fmt.Errorf("cancel of non-existent htlc")
×
173
                }
×
174

UNCOV
175
                err := canCancelSingleHtlc(htlc, invoice.State)
×
UNCOV
176
                if err != nil {
×
177
                        return err
×
178
                }
×
179

UNCOV
180
                err = resolveHtlc(
×
UNCOV
181
                        key, htlc, HtlcStateCanceled, updateTime,
×
UNCOV
182
                        updater,
×
UNCOV
183
                )
×
UNCOV
184
                if err != nil {
×
185
                        return err
×
186
                }
×
187

188
                // Tally this into the set of HTLCs that need to be updated on
189
                // disk, but once again, only if this is an AMP invoice.
UNCOV
190
                if invoice.IsAMP() {
×
UNCOV
191
                        err := cancelHtlcsAmp(invoice, key, htlc, updater)
×
UNCOV
192
                        if err != nil {
×
193
                                return err
×
194
                        }
×
195
                }
196
        }
197

198
        return nil
3✔
199
}
200

201
// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc.
202
//
203
//nolint:funlen
204
func addHTLCs(invoice *Invoice, hash *lntypes.Hash, updateTime time.Time,
205
        update *InvoiceUpdateDesc, updater InvoiceUpdater) error {
3✔
206

3✔
207
        var setID *[32]byte
3✔
208
        invoiceIsAMP := invoice.IsAMP()
3✔
209
        if invoiceIsAMP && update.State != nil {
6✔
210
                setID = update.State.SetID
3✔
211
        }
3✔
212

213
        for key, htlcUpdate := range update.AddHtlcs {
6✔
214
                if _, exists := invoice.Htlcs[key]; exists {
3✔
215
                        return fmt.Errorf("duplicate add of htlc %v", key)
×
216
                }
×
217

218
                // Force caller to supply htlc without custom records in a
219
                // consistent way.
220
                if htlcUpdate.CustomRecords == nil {
3✔
221
                        return errors.New("nil custom records map")
×
222
                }
×
223

224
                htlc := &InvoiceHTLC{
3✔
225
                        Amt:           htlcUpdate.Amt,
3✔
226
                        MppTotalAmt:   htlcUpdate.MppTotalAmt,
3✔
227
                        Expiry:        htlcUpdate.Expiry,
3✔
228
                        AcceptHeight:  uint32(htlcUpdate.AcceptHeight),
3✔
229
                        AcceptTime:    updateTime,
3✔
230
                        State:         HtlcStateAccepted,
3✔
231
                        CustomRecords: htlcUpdate.CustomRecords,
3✔
232
                }
3✔
233

3✔
234
                if invoiceIsAMP {
6✔
235
                        if htlcUpdate.AMP == nil {
3✔
236
                                return fmt.Errorf("unable to add htlc "+
×
237
                                        "without AMP data to AMP invoice(%v)",
×
238
                                        invoice.AddIndex)
×
239
                        }
×
240

241
                        htlc.AMP = htlcUpdate.AMP.Copy()
3✔
242
                }
243

244
                if err := updater.AddHtlc(key, htlc); err != nil {
3✔
UNCOV
245
                        return err
×
UNCOV
246
                }
×
247

248
                invoice.Htlcs[key] = htlc
3✔
249

3✔
250
                // Collect the set of new HTLCs so we can write them properly
3✔
251
                // below, but only if this is an AMP invoice.
3✔
252
                if invoiceIsAMP {
6✔
253
                        err := acceptHtlcsAmp(
3✔
254
                                invoice, htlcUpdate.AMP.Record.SetID(), key,
3✔
255
                                htlc, updater,
3✔
256
                        )
3✔
257
                        if err != nil {
3✔
258
                                return err
×
259
                        }
×
260
                }
261
        }
262

263
        // At this point, the set of accepted HTLCs should be fully
264
        // populated with added HTLCs or removed of canceled ones. Update
265
        // invoice state if the update descriptor indicates an invoice state
266
        // change, which depends on having an accurate view of the accepted
267
        // HTLCs.
268
        if update.State != nil {
6✔
269
                newState, err := getUpdatedInvoiceState(
3✔
270
                        invoice, hash, *update.State,
3✔
271
                )
3✔
272
                if err != nil {
3✔
UNCOV
273
                        return err
×
UNCOV
274
                }
×
275

276
                // If this isn't an AMP invoice, then we'll go ahead and update
277
                // the invoice state directly here. For AMP invoices, we instead
278
                // will keep the top-level invoice open, and update the state of
279
                // each _htlc set_ instead. However, we'll allow the invoice to
280
                // transition to the cancelled state regardless.
281
                if !invoiceIsAMP || *newState == ContractCanceled {
6✔
282
                        err := updater.UpdateInvoiceState(*newState, nil)
3✔
283
                        if err != nil {
3✔
284
                                return err
×
285
                        }
×
286
                        invoice.State = *newState
3✔
287
                }
288
        }
289

290
        // The set of HTLC pre-images will only be set if we were actually able
291
        // to reconstruct all the AMP pre-images.
292
        var settleEligibleAMP bool
3✔
293
        if update.State != nil {
6✔
294
                settleEligibleAMP = len(update.State.HTLCPreimages) != 0
3✔
295
        }
3✔
296

297
        // With any invoice level state transitions recorded, we'll now
298
        // finalize the process by updating the state transitions for
299
        // individual HTLCs
300
        var amtPaid lnwire.MilliSatoshi
3✔
301

3✔
302
        for key, htlc := range invoice.Htlcs {
6✔
303
                // Set the HTLC preimage for any AMP HTLCs.
3✔
304
                if setID != nil && update.State != nil {
6✔
305
                        preimage, ok := update.State.HTLCPreimages[key]
3✔
306
                        switch {
3✔
307
                        // If we don't already have a preimage for this HTLC, we
308
                        // can set it now.
309
                        case ok && htlc.AMP.Preimage == nil:
3✔
310
                                err := updater.AddAmpHtlcPreimage(
3✔
311
                                        htlc.AMP.Record.SetID(), key, preimage,
3✔
312
                                )
3✔
313
                                if err != nil {
3✔
314
                                        return err
×
315
                                }
×
316
                                htlc.AMP.Preimage = &preimage
3✔
317

318
                        // Otherwise, prevent over-writing an existing
319
                        // preimage.  Ignore the case where the preimage is
320
                        // identical.
UNCOV
321
                        case ok && *htlc.AMP.Preimage != preimage:
×
UNCOV
322
                                return ErrHTLCPreimageAlreadyExists
×
323
                        }
324
                }
325

326
                // The invoice state may have changed and this could have
327
                // implications for the states of the individual htlcs. Align
328
                // the htlc state with the current invoice state.
329
                //
330
                // If we have all the pre-images for an AMP invoice, then we'll
331
                // act as if we're able to settle the entire invoice. We need
332
                // to do this since it's possible for us to settle AMP invoices
333
                // while the contract state (on disk) is still in the accept
334
                // state.
335
                htlcContextState := invoice.State
3✔
336
                if settleEligibleAMP {
6✔
337
                        htlcContextState = ContractSettled
3✔
338
                }
3✔
339
                htlcStateChanged, htlcState, err := getUpdatedHtlcState(
3✔
340
                        htlc, htlcContextState, setID,
3✔
341
                )
3✔
342
                if err != nil {
3✔
343
                        return err
×
344
                }
×
345

346
                if htlcStateChanged {
6✔
347
                        err = resolveHtlc(
3✔
348
                                key, htlc, htlcState, updateTime, updater,
3✔
349
                        )
3✔
350
                        if err != nil {
3✔
351
                                return err
×
352
                        }
×
353
                }
354

355
                htlcSettled := htlcStateChanged &&
3✔
356
                        htlcState == HtlcStateSettled
3✔
357

3✔
358
                // If the HTLC has being settled for the first time, and this
3✔
359
                // is an AMP invoice, then we'll need to update some additional
3✔
360
                // meta data state.
3✔
361
                if htlcSettled && invoiceIsAMP {
6✔
362
                        err = settleHtlcsAmp(invoice, key, htlc, updater)
3✔
363
                        if err != nil {
3✔
364
                                return err
×
365
                        }
×
366
                }
367

368
                accepted := htlc.State == HtlcStateAccepted
3✔
369
                settled := htlc.State == HtlcStateSettled
3✔
370
                invoiceStateReady := accepted || settled
3✔
371

3✔
372
                if !invoiceIsAMP {
6✔
373
                        // Update the running amount paid to this invoice. We
3✔
374
                        // don't include accepted htlcs when the invoice is
3✔
375
                        // still open.
3✔
376
                        if invoice.State != ContractOpen &&
3✔
377
                                invoiceStateReady {
6✔
378

3✔
379
                                amtPaid += htlc.Amt
3✔
380
                        }
3✔
381
                } else {
3✔
382
                        // For AMP invoices, since we won't always be reading
3✔
383
                        // out the total invoice set each time, we'll instead
3✔
384
                        // accumulate newly added invoices to the total amount
3✔
385
                        // paid.
3✔
386
                        if _, ok := update.AddHtlcs[key]; !ok {
6✔
387
                                continue
3✔
388
                        }
389

390
                        // Update the running amount paid to this invoice. AMP
391
                        // invoices never go to the settled state, so if it's
392
                        // open, then we tally the HTLC.
393
                        if invoice.State == ContractOpen &&
3✔
394
                                invoiceStateReady {
6✔
395

3✔
396
                                amtPaid += htlc.Amt
3✔
397
                        }
3✔
398
                }
399
        }
400

401
        // For non-AMP invoices we recalculate the amount paid from scratch
402
        // each time, while for AMP invoices, we'll accumulate only based on
403
        // newly added HTLCs.
404
        if invoiceIsAMP {
6✔
405
                amtPaid += invoice.AmtPaid
3✔
406
        }
3✔
407

408
        return updateInvoiceAmtPaid(invoice, amtPaid, updater)
3✔
409
}
410

411
func resolveHtlc(circuitKey models.CircuitKey, htlc *InvoiceHTLC,
412
        state HtlcState, resolveTime time.Time,
413
        updater InvoiceUpdater) error {
3✔
414

3✔
415
        err := updater.ResolveHtlc(circuitKey, state, resolveTime)
3✔
416
        if err != nil {
3✔
417
                return err
×
418
        }
×
419
        htlc.State = state
3✔
420
        htlc.ResolveTime = resolveTime
3✔
421

3✔
422
        return nil
3✔
423
}
424

425
func updateInvoiceAmtPaid(invoice *Invoice, amt lnwire.MilliSatoshi,
426
        updater InvoiceUpdater) error {
3✔
427

3✔
428
        err := updater.UpdateInvoiceAmtPaid(amt)
3✔
429
        if err != nil {
3✔
430
                return err
×
431
        }
×
432
        invoice.AmtPaid = amt
3✔
433

3✔
434
        return nil
3✔
435
}
436

437
// settleHodlInvoice marks a hodl invoice as settled.
438
//
439
// NOTE: Currently it is not possible to have HODL AMP invoices.
440
func settleHodlInvoice(invoice *Invoice, hash *lntypes.Hash,
441
        updateTime time.Time, update *InvoiceStateUpdateDesc,
442
        updater InvoiceUpdater) error {
3✔
443

3✔
444
        if !invoice.HodlInvoice {
3✔
445
                return fmt.Errorf("unable to settle hodl invoice: %v is "+
×
446
                        "not a hodl invoice", invoice.AddIndex)
×
447
        }
×
448

449
        // TODO(positiveblue): because NewState can only be ContractSettled we
450
        // can remove it from the API and set it here directly.
451
        switch {
3✔
452
        case update == nil:
×
453
                fallthrough
×
454

455
        case update.NewState != ContractSettled:
×
456
                return fmt.Errorf("unable to settle hodl invoice: "+
×
457
                        "not valid InvoiceUpdateDesc.State: %v", update)
×
458

459
        case update.Preimage == nil:
×
460
                return fmt.Errorf("unable to settle hodl invoice: " +
×
461
                        "preimage is nil")
×
462
        }
463

464
        newState, err := getUpdatedInvoiceState(
3✔
465
                invoice, hash, *update,
3✔
466
        )
3✔
467
        if err != nil {
3✔
468
                return err
×
469
        }
×
470

471
        if newState == nil || *newState != ContractSettled {
3✔
472
                return fmt.Errorf("unable to settle hodl invoice: "+
×
473
                        "new computed state is not settled: %s", newState)
×
474
        }
×
475

476
        err = updater.UpdateInvoiceState(
3✔
477
                ContractSettled, update.Preimage,
3✔
478
        )
3✔
479
        if err != nil {
3✔
480
                return err
×
481
        }
×
482

483
        invoice.State = ContractSettled
3✔
484
        invoice.Terms.PaymentPreimage = update.Preimage
3✔
485

3✔
486
        // TODO(positiveblue): this logic can be further simplified.
3✔
487
        var amtPaid lnwire.MilliSatoshi
3✔
488
        for key, htlc := range invoice.Htlcs {
6✔
489
                settled, _, err := getUpdatedHtlcState(
3✔
490
                        htlc, ContractSettled, nil,
3✔
491
                )
3✔
492
                if err != nil {
3✔
493
                        return err
×
494
                }
×
495

496
                if settled {
6✔
497
                        err = resolveHtlc(
3✔
498
                                key, htlc, HtlcStateSettled, updateTime,
3✔
499
                                updater,
3✔
500
                        )
3✔
501
                        if err != nil {
3✔
502
                                return err
×
503
                        }
×
504

505
                        amtPaid += htlc.Amt
3✔
506
                }
507
        }
508

509
        return updateInvoiceAmtPaid(invoice, amtPaid, updater)
3✔
510
}
511

512
// cancelInvoice attempts to cancel the given invoice. That includes changing
513
// the invoice state and the state of any relevant HTLC.
514
func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
515
        updateTime time.Time, update *InvoiceStateUpdateDesc,
516
        updater InvoiceUpdater) error {
3✔
517

3✔
518
        switch {
3✔
519
        case update == nil:
×
520
                fallthrough
×
521

522
        case update.NewState != ContractCanceled:
×
523
                return fmt.Errorf("unable to cancel invoice: "+
×
524
                        "InvoiceUpdateDesc.State not valid: %v", update)
×
525
        }
526

527
        var (
3✔
528
                setID        *[32]byte
3✔
529
                invoiceIsAMP bool
3✔
530
        )
3✔
531

3✔
532
        invoiceIsAMP = invoice.IsAMP()
3✔
533
        if invoiceIsAMP {
3✔
UNCOV
534
                setID = update.SetID
×
UNCOV
535
        }
×
536

537
        newState, err := getUpdatedInvoiceState(invoice, hash, *update)
3✔
538
        if err != nil {
6✔
539
                return err
3✔
540
        }
3✔
541

542
        if newState == nil || *newState != ContractCanceled {
3✔
543
                return fmt.Errorf("unable to cancel invoice(%v): new "+
×
544
                        "computed state is not canceled: %s", invoice.AddIndex,
×
545
                        newState)
×
546
        }
×
547

548
        err = updater.UpdateInvoiceState(ContractCanceled, nil)
3✔
549
        if err != nil {
3✔
550
                return err
×
551
        }
×
552
        invoice.State = ContractCanceled
3✔
553

3✔
554
        for key, htlc := range invoice.Htlcs {
6✔
555
                canceled, _, err := getUpdatedHtlcState(
3✔
556
                        htlc, ContractCanceled, setID,
3✔
557
                )
3✔
558
                if err != nil {
3✔
559
                        return err
×
560
                }
×
561

562
                if canceled {
6✔
563
                        err = resolveHtlc(
3✔
564
                                key, htlc, HtlcStateCanceled, updateTime,
3✔
565
                                updater,
3✔
566
                        )
3✔
567
                        if err != nil {
3✔
568
                                return err
×
569
                        }
×
570

571
                        if invoice.IsAMP() {
3✔
NEW
572
                                err := cancelHtlcsAmp(
×
NEW
573
                                        invoice, key, htlc, updater,
×
NEW
574
                                )
×
NEW
575
                                if err != nil {
×
NEW
576
                                        return err
×
NEW
577
                                }
×
578
                        }
579
                }
580
        }
581

582
        return nil
3✔
583
}
584

585
// getUpdatedInvoiceState validates and processes an invoice state update. The
586
// new state to transition to is returned, so the caller is able to select
587
// exactly how the invoice state is updated. Note that for AMP invoices this
588
// function is only used to validate the state transition if we're cancelling
589
// the invoice.
590
func getUpdatedInvoiceState(invoice *Invoice, hash *lntypes.Hash,
591
        update InvoiceStateUpdateDesc) (*ContractState, error) {
3✔
592

3✔
593
        // Returning to open is never allowed from any state.
3✔
594
        if update.NewState == ContractOpen {
3✔
595
                return nil, ErrInvoiceCannotOpen
×
596
        }
×
597

598
        switch invoice.State {
3✔
599
        // Once a contract is accepted, we can only transition to settled or
600
        // canceled. Forbid transitioning back into this state. Otherwise this
601
        // state is identical to ContractOpen, so we fallthrough to apply the
602
        // same checks that we apply to open invoices.
603
        case ContractAccepted:
3✔
604
                if update.NewState == ContractAccepted {
3✔
605
                        return nil, ErrInvoiceCannotAccept
×
606
                }
×
607

608
                fallthrough
3✔
609

610
        // If a contract is open, permit a state transition to accepted, settled
611
        // or canceled. The only restriction is on transitioning to settled
612
        // where we ensure the preimage is valid.
613
        case ContractOpen:
3✔
614
                if update.NewState == ContractCanceled {
6✔
615
                        return &update.NewState, nil
3✔
616
                }
3✔
617

618
                // Sanity check that the user isn't trying to settle or accept a
619
                // non-existent HTLC set.
620
                set := invoice.HTLCSet(update.SetID, HtlcStateAccepted)
3✔
621
                if len(set) == 0 {
3✔
UNCOV
622
                        return nil, ErrEmptyHTLCSet
×
UNCOV
623
                }
×
624

625
                // For AMP invoices, there are no invoice-level preimage checks.
626
                // However, we still sanity check that we aren't trying to
627
                // settle an AMP invoice with a preimage.
628
                if update.SetID != nil {
6✔
629
                        if update.Preimage != nil {
3✔
630
                                return nil, errors.New("AMP set cannot have " +
×
631
                                        "preimage")
×
632
                        }
×
633

634
                        return &update.NewState, nil
3✔
635
                }
636

637
                switch {
3✔
638
                // If an invoice-level preimage was supplied, but the InvoiceRef
639
                // doesn't specify a hash (e.g. AMP invoices) we fail.
UNCOV
640
                case update.Preimage != nil && hash == nil:
×
UNCOV
641
                        return nil, ErrUnexpectedInvoicePreimage
×
642

643
                // Validate the supplied preimage for non-AMP invoices.
644
                case update.Preimage != nil:
3✔
645
                        if update.Preimage.Hash() != *hash {
3✔
646
                                return nil, ErrInvoicePreimageMismatch
×
647
                        }
×
648

649
                // Permit non-AMP invoices to be accepted without knowing the
650
                // preimage. When trying to settle we'll have to pass through
651
                // the above check in order to not hit the one below.
652
                case update.NewState == ContractAccepted:
3✔
653

654
                // Fail if we still don't have a preimage when transitioning to
655
                // settle the non-AMP invoice.
656
                case update.NewState == ContractSettled &&
657
                        invoice.Terms.PaymentPreimage == nil:
×
658

×
659
                        return nil, errors.New("unknown preimage")
×
660
                }
661

662
                return &update.NewState, nil
3✔
663

664
        // Once settled, we are in a terminal state.
665
        case ContractSettled:
3✔
666
                return nil, ErrInvoiceAlreadySettled
3✔
667

668
        // Once canceled, we are in a terminal state.
UNCOV
669
        case ContractCanceled:
×
UNCOV
670
                return nil, ErrInvoiceAlreadyCanceled
×
671

672
        default:
×
673
                return nil, errors.New("unknown state transition")
×
674
        }
675
}
676

677
// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without
678
// applying it), given the new state, and the amount of the HTLC that is
679
// being updated.
680
func getUpdatedInvoiceAmpState(invoice *Invoice, setID SetID,
681
        circuitKey models.CircuitKey, state HtlcState,
682
        amt lnwire.MilliSatoshi) (InvoiceStateAMP, error) {
3✔
683

3✔
684
        // Retrieve the AMP state for this set ID.
3✔
685
        ampState, ok := invoice.AMPState[setID]
3✔
686

3✔
687
        // If the state is accepted then we may need to create a new entry for
3✔
688
        // this set ID, otherwise we expect that the entry already exists and
3✔
689
        // we can update it.
3✔
690
        if !ok && state != HtlcStateAccepted {
3✔
691
                return InvoiceStateAMP{},
×
692
                        fmt.Errorf("unable to update AMP state for setID=%x ",
×
693
                                setID)
×
694
        }
×
695

696
        switch state {
3✔
697
        case HtlcStateAccepted:
3✔
698
                if !ok {
6✔
699
                        // If an entry for this set ID doesn't already exist,
3✔
700
                        // then we'll need to create it.
3✔
701
                        ampState = InvoiceStateAMP{
3✔
702
                                State: HtlcStateAccepted,
3✔
703
                                InvoiceKeys: make(
3✔
704
                                        map[models.CircuitKey]struct{},
3✔
705
                                ),
3✔
706
                        }
3✔
707
                }
3✔
708

709
                ampState.AmtPaid += amt
3✔
710

UNCOV
711
        case HtlcStateCanceled:
×
UNCOV
712
                ampState.State = HtlcStateCanceled
×
UNCOV
713
                ampState.AmtPaid -= amt
×
714

715
        case HtlcStateSettled:
3✔
716
                ampState.State = HtlcStateSettled
3✔
717
        }
718

719
        ampState.InvoiceKeys[circuitKey] = struct{}{}
3✔
720

3✔
721
        return ampState, nil
3✔
722
}
723

724
// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is
725
// returned, then the HTLC can be cancelled.
726
func canCancelSingleHtlc(htlc *InvoiceHTLC,
UNCOV
727
        invoiceState ContractState) error {
×
UNCOV
728

×
UNCOV
729
        // It is only possible to cancel individual htlcs on an open invoice.
×
UNCOV
730
        if invoiceState != ContractOpen {
×
731
                return fmt.Errorf("htlc canceled on invoice in state %v",
×
732
                        invoiceState)
×
733
        }
×
734

735
        // It is only possible if the htlc is still pending.
UNCOV
736
        if htlc.State != HtlcStateAccepted {
×
737
                return fmt.Errorf("htlc canceled in state %v", htlc.State)
×
738
        }
×
739

UNCOV
740
        return nil
×
741
}
742

743
// getUpdatedHtlcState aligns the state of an htlc with the given invoice state.
744
// A boolean indicating whether the HTLCs state need to be updated, along with
745
// the new state (or old state if no change is needed) is returned.
746
func getUpdatedHtlcState(htlc *InvoiceHTLC,
747
        invoiceState ContractState, setID *[32]byte) (
748
        bool, HtlcState, error) {
3✔
749

3✔
750
        trySettle := func(persist bool) (bool, HtlcState, error) {
6✔
751
                if htlc.State != HtlcStateAccepted {
3✔
UNCOV
752
                        return false, htlc.State, nil
×
UNCOV
753
                }
×
754

755
                // Settle the HTLC if it matches the settled set id. If
756
                // there're other HTLCs with distinct setIDs, then we'll leave
757
                // them, as they may eventually be settled as we permit
758
                // multiple settles to a single pay_addr for AMP.
759
                settled := false
3✔
760
                if htlc.IsInHTLCSet(setID) {
6✔
761
                        // Non-AMP HTLCs can be settled immediately since we
3✔
762
                        // already know the preimage is valid due to checks at
3✔
763
                        // the invoice level. For AMP HTLCs, verify that the
3✔
764
                        // per-HTLC preimage-hash pair is valid.
3✔
765
                        switch {
3✔
766
                        // Non-AMP HTLCs can be settle immediately since we
767
                        // already know the preimage is valid due to checks at
768
                        // the invoice level.
769
                        case setID == nil:
3✔
770

771
                        // At this point, the setID is non-nil, meaning this is
772
                        // an AMP HTLC. We know that htlc.AMP cannot be nil,
773
                        // otherwise IsInHTLCSet would have returned false.
774
                        //
775
                        // Fail if an accepted AMP HTLC has no preimage.
UNCOV
776
                        case htlc.AMP.Preimage == nil:
×
UNCOV
777
                                return false, htlc.State,
×
UNCOV
778
                                        ErrHTLCPreimageMissing
×
779

780
                        // Fail if the accepted AMP HTLC has an invalid
781
                        // preimage.
UNCOV
782
                        case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash):
×
UNCOV
783
                                return false, htlc.State,
×
UNCOV
784
                                        ErrHTLCPreimageMismatch
×
785
                        }
786

787
                        settled = true
3✔
788
                }
789

790
                // Only persist the changes if the invoice is moving to the
791
                // settled state, and we're actually updating the state to
792
                // settled.
793
                newState := htlc.State
3✔
794
                if settled {
6✔
795
                        newState = HtlcStateSettled
3✔
796
                }
3✔
797

798
                return persist && settled, newState, nil
3✔
799
        }
800

801
        if invoiceState == ContractSettled {
6✔
802
                // Check that we can settle the HTLCs. For legacy and MPP HTLCs
3✔
803
                // this will be a NOP, but for AMP HTLCs this asserts that we
3✔
804
                // have a valid hash/preimage pair. Passing true permits the
3✔
805
                // method to update the HTLC to HtlcStateSettled.
3✔
806
                return trySettle(true)
3✔
807
        }
3✔
808

809
        // We should never find a settled HTLC on an invoice that isn't in
810
        // ContractSettled.
811
        if htlc.State == HtlcStateSettled {
3✔
UNCOV
812
                return false, htlc.State, ErrHTLCAlreadySettled
×
UNCOV
813
        }
×
814

815
        switch invoiceState {
3✔
816
        case ContractCanceled:
3✔
817
                htlcAlreadyCanceled := htlc.State == HtlcStateCanceled
3✔
818
                return !htlcAlreadyCanceled, HtlcStateCanceled, nil
3✔
819

820
        // TODO(roasbeef): never fully passed thru now?
821
        case ContractAccepted:
3✔
822
                // Check that we can settle the HTLCs. For legacy and MPP HTLCs
3✔
823
                // this will be a NOP, but for AMP HTLCs this asserts that we
3✔
824
                // have a valid hash/preimage pair. Passing false prevents the
3✔
825
                // method from putting the HTLC in HtlcStateSettled, leaving it
3✔
826
                // in HtlcStateAccepted.
3✔
827
                return trySettle(false)
3✔
828

829
        case ContractOpen:
3✔
830
                return false, htlc.State, nil
3✔
831

832
        default:
×
833
                return false, htlc.State, errors.New("unknown state transition")
×
834
        }
835
}
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