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

lightningnetwork / lnd / 13536249039

26 Feb 2025 03:42AM UTC coverage: 57.462% (-1.4%) from 58.835%
13536249039

Pull #8453

github

Roasbeef
peer: update chooseDeliveryScript to gen script if needed

In this commit, we update `chooseDeliveryScript` to generate a new
script if needed. This allows us to fold in a few other lines that
always followed this function into this expanded function.

The tests have been updated accordingly.
Pull Request #8453: [4/4] - multi: integrate new rbf coop close FSM into the existing peer flow

275 of 1318 new or added lines in 22 files covered. (20.86%)

19521 existing lines in 257 files now uncovered.

103858 of 180741 relevant lines covered (57.46%)

24750.23 hits per line

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

86.26
/invoices/invoice_expiry_watcher.go
1
package invoices
2

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

9
        "github.com/btcsuite/btcd/chaincfg/chainhash"
10
        "github.com/lightningnetwork/lnd/chainntnfs"
11
        "github.com/lightningnetwork/lnd/clock"
12
        "github.com/lightningnetwork/lnd/lntypes"
13
        "github.com/lightningnetwork/lnd/queue"
14
        "github.com/lightningnetwork/lnd/zpay32"
15
)
16

17
// invoiceExpiry is a vanity interface for different invoice expiry types
18
// which implement the priority queue item interface, used to improve code
19
// readability.
20
type invoiceExpiry queue.PriorityQueueItem
21

22
// Compile time assertion that invoiceExpiryTs implements invoiceExpiry.
23
var _ invoiceExpiry = (*invoiceExpiryTs)(nil)
24

25
// invoiceExpiryTs holds and invoice's payment hash and its expiry. This
26
// is used to order invoices by their expiry time for cancellation.
27
type invoiceExpiryTs struct {
28
        PaymentHash lntypes.Hash
29
        Expiry      time.Time
30
        Keysend     bool
31
}
32

33
// Less implements PriorityQueueItem.Less such that the top item in the
34
// priority queue will be the one that expires next.
35
func (e invoiceExpiryTs) Less(other queue.PriorityQueueItem) bool {
501✔
36
        return e.Expiry.Before(other.(*invoiceExpiryTs).Expiry)
501✔
37
}
501✔
38

39
// Compile time assertion that invoiceExpiryHeight implements invoiceExpiry.
40
var _ invoiceExpiry = (*invoiceExpiryHeight)(nil)
41

42
// invoiceExpiryHeight holds information about an invoice which can be used to
43
// cancel it based on its expiry height.
44
type invoiceExpiryHeight struct {
45
        paymentHash  lntypes.Hash
46
        expiryHeight uint32
47
}
48

49
// Less implements PriorityQueueItem.Less such that the top item in the
50
// priority queue is the lowest block height.
51
func (b invoiceExpiryHeight) Less(other queue.PriorityQueueItem) bool {
51✔
52
        return b.expiryHeight < other.(*invoiceExpiryHeight).expiryHeight
51✔
53
}
51✔
54

55
// expired returns a boolean that indicates whether this entry has expired,
56
// taking our expiry delta into account.
57
func (b invoiceExpiryHeight) expired(currentHeight, delta uint32) bool {
395✔
58
        return currentHeight+delta >= b.expiryHeight
395✔
59
}
395✔
60

61
// InvoiceExpiryWatcher handles automatic invoice cancellation of expired
62
// invoices. Upon start InvoiceExpiryWatcher will retrieve all pending (not yet
63
// settled or canceled) invoices invoices to its watching queue. When a new
64
// invoice is added to the InvoiceRegistry, it'll be forwarded to the
65
// InvoiceExpiryWatcher and will end up in the watching queue as well.
66
// If any of the watched invoices expire, they'll be removed from the watching
67
// queue and will be cancelled through InvoiceRegistry.CancelInvoice().
68
type InvoiceExpiryWatcher struct {
69
        sync.Mutex
70
        started bool
71

72
        // clock is the clock implementation that InvoiceExpiryWatcher uses.
73
        // It is useful for testing.
74
        clock clock.Clock
75

76
        // notifier provides us with block height updates.
77
        notifier chainntnfs.ChainNotifier
78

79
        // blockExpiryDelta is the number of blocks before a htlc's expiry that
80
        // we expire the invoice based on expiry height. We use a delta because
81
        // we will go to some delta before our expiry, so we want to cancel
82
        // before this to prevent force closes.
83
        blockExpiryDelta uint32
84

85
        // currentHeight is the current block height.
86
        currentHeight uint32
87

88
        // currentHash is the block hash for our current height.
89
        currentHash *chainhash.Hash
90

91
        // cancelInvoice is a template method that cancels an expired invoice.
92
        cancelInvoice func(lntypes.Hash, bool) error
93

94
        // timestampExpiryQueue holds invoiceExpiry items and is used to find
95
        // the next invoice to expire.
96
        timestampExpiryQueue queue.PriorityQueue
97

98
        // blockExpiryQueue holds blockExpiry items and is used to find the
99
        // next invoice to expire based on block height. Only hold invoices
100
        // with active htlcs are added to this queue, because they require
101
        // manual cancellation when the hltc is going to time out. Items in
102
        // this queue may already be in the timestampExpiryQueue, this is ok
103
        // because they will not be expired based on timestamp if they have
104
        // active htlcs.
105
        blockExpiryQueue queue.PriorityQueue
106

107
        // newInvoices channel is used to wake up the main loop when a new
108
        // invoices is added.
109
        newInvoices chan []invoiceExpiry
110

111
        wg sync.WaitGroup
112

113
        // quit signals InvoiceExpiryWatcher to stop.
114
        quit chan struct{}
115
}
116

117
// NewInvoiceExpiryWatcher creates a new InvoiceExpiryWatcher instance.
118
func NewInvoiceExpiryWatcher(clock clock.Clock,
119
        expiryDelta, startHeight uint32, startHash *chainhash.Hash,
120
        notifier chainntnfs.ChainNotifier) *InvoiceExpiryWatcher {
654✔
121

654✔
122
        return &InvoiceExpiryWatcher{
654✔
123
                clock:            clock,
654✔
124
                notifier:         notifier,
654✔
125
                blockExpiryDelta: expiryDelta,
654✔
126
                currentHeight:    startHeight,
654✔
127
                currentHash:      startHash,
654✔
128
                newInvoices:      make(chan []invoiceExpiry),
654✔
129
                quit:             make(chan struct{}),
654✔
130
        }
654✔
131
}
654✔
132

133
// Start starts the subscription handler and the main loop. Start() will
134
// return with error if InvoiceExpiryWatcher is already started. Start()
135
// expects a cancellation function passed that will be use to cancel expired
136
// invoices by their payment hash.
137
func (ew *InvoiceExpiryWatcher) Start(
138
        cancelInvoice func(lntypes.Hash, bool) error) error {
656✔
139

656✔
140
        ew.Lock()
656✔
141
        defer ew.Unlock()
656✔
142

656✔
143
        if ew.started {
657✔
144
                return fmt.Errorf("InvoiceExpiryWatcher already started")
1✔
145
        }
1✔
146

147
        ew.started = true
655✔
148
        ew.cancelInvoice = cancelInvoice
655✔
149

655✔
150
        ntfn, err := ew.notifier.RegisterBlockEpochNtfn(&chainntnfs.BlockEpoch{
655✔
151
                Height: int32(ew.currentHeight),
655✔
152
                Hash:   ew.currentHash,
655✔
153
        })
655✔
154
        if err != nil {
655✔
155
                return err
×
156
        }
×
157

158
        ew.wg.Add(1)
655✔
159
        go ew.mainLoop(ntfn)
655✔
160

655✔
161
        return nil
655✔
162
}
163

164
// Stop quits the expiry handler loop and waits for InvoiceExpiryWatcher to
165
// fully stop.
166
func (ew *InvoiceExpiryWatcher) Stop() {
393✔
167
        ew.Lock()
393✔
168
        defer ew.Unlock()
393✔
169

393✔
170
        if ew.started {
786✔
171
                // Signal subscriptionHandler to quit and wait for it to return.
393✔
172
                close(ew.quit)
393✔
173
                ew.wg.Wait()
393✔
174
                ew.started = false
393✔
175
        }
393✔
176
}
177

178
// makeInvoiceExpiry checks if the passed invoice may be canceled and calculates
179
// the expiry time and creates a slimmer invoiceExpiry implementation.
180
func makeInvoiceExpiry(paymentHash lntypes.Hash,
181
        invoice *Invoice) invoiceExpiry {
855✔
182

855✔
183
        switch invoice.State {
855✔
184
        // If we have an open invoice with no htlcs, we want to expire the
185
        // invoice based on timestamp
186
        case ContractOpen:
771✔
187
                return makeTimestampExpiry(paymentHash, invoice)
771✔
188

189
        // If an invoice has active htlcs, we want to expire it based on block
190
        // height. We only do this for hodl invoices, since regular invoices
191
        // should resolve themselves automatically.
192
        case ContractAccepted:
84✔
193
                if !invoice.HodlInvoice {
84✔
194
                        log.Debugf("Invoice in accepted state not added to "+
×
195
                                "expiry watcher: %v", paymentHash)
×
196

×
197
                        return nil
×
198
                }
×
199

200
                var minHeight uint32
84✔
201
                for _, htlc := range invoice.Htlcs {
179✔
202
                        // We only care about accepted htlcs, since they will
95✔
203
                        // trigger force-closes.
95✔
204
                        if htlc.State != HtlcStateAccepted {
98✔
205
                                continue
3✔
206
                        }
207

208
                        if minHeight == 0 || htlc.Expiry < minHeight {
176✔
209
                                minHeight = htlc.Expiry
84✔
210
                        }
84✔
211
                }
212

213
                return makeHeightExpiry(paymentHash, minHeight)
84✔
214

215
        default:
×
216
                log.Debugf("Invoice not added to expiry watcher: %v",
×
217
                        paymentHash)
×
218

×
219
                return nil
×
220
        }
221
}
222

223
// makeTimestampExpiry creates a timestamp-based expiry entry.
224
func makeTimestampExpiry(paymentHash lntypes.Hash,
225
        invoice *Invoice) *invoiceExpiryTs {
771✔
226

771✔
227
        if invoice.State != ContractOpen {
771✔
228
                return nil
×
229
        }
×
230

231
        realExpiry := invoice.Terms.Expiry
771✔
232
        if realExpiry == 0 {
1,116✔
233
                realExpiry = zpay32.DefaultInvoiceExpiry
345✔
234
        }
345✔
235

236
        expiry := invoice.CreationDate.Add(realExpiry)
771✔
237
        return &invoiceExpiryTs{
771✔
238
                PaymentHash: paymentHash,
771✔
239
                Expiry:      expiry,
771✔
240
                Keysend:     len(invoice.PaymentRequest) == 0,
771✔
241
        }
771✔
242
}
243

244
// makeHeightExpiry creates height-based expiry for an invoice based on its
245
// lowest htlc expiry height.
246
func makeHeightExpiry(paymentHash lntypes.Hash,
247
        minHeight uint32) *invoiceExpiryHeight {
86✔
248

86✔
249
        if minHeight == 0 {
87✔
250
                log.Warnf("make height expiry called with 0 height")
1✔
251
                return nil
1✔
252
        }
1✔
253

254
        return &invoiceExpiryHeight{
85✔
255
                paymentHash:  paymentHash,
85✔
256
                expiryHeight: minHeight,
85✔
257
        }
85✔
258
}
259

260
// AddInvoices adds invoices to the InvoiceExpiryWatcher.
261
func (ew *InvoiceExpiryWatcher) AddInvoices(invoices ...invoiceExpiry) {
1,464✔
262
        if len(invoices) == 0 {
2,106✔
263
                return
642✔
264
        }
642✔
265

266
        select {
822✔
267
        case ew.newInvoices <- invoices:
822✔
268
                log.Debugf("Added %d invoices to the expiry watcher",
822✔
269
                        len(invoices))
822✔
270

271
        // Select on quit too so that callers won't get blocked in case
272
        // of concurrent shutdown.
273
        case <-ew.quit:
×
274
        }
275
}
276

277
// nextTimestampExpiry returns a Time chan to wait on until the next invoice
278
// expires. If there are no active invoices, then it'll simply wait
279
// indefinitely.
280
func (ew *InvoiceExpiryWatcher) nextTimestampExpiry() <-chan time.Time {
1,823✔
281
        if !ew.timestampExpiryQueue.Empty() {
2,928✔
282
                top := ew.timestampExpiryQueue.Top().(*invoiceExpiryTs)
1,105✔
283
                return ew.clock.TickAfter(top.Expiry.Sub(ew.clock.Now()))
1,105✔
284
        }
1,105✔
285

286
        return nil
718✔
287
}
288

289
// nextHeightExpiry returns a channel that will immediately be read from if
290
// the top item on our queue has expired.
291
func (ew *InvoiceExpiryWatcher) nextHeightExpiry() <-chan uint32 {
1,823✔
292
        if ew.blockExpiryQueue.Empty() {
3,266✔
293
                return nil
1,443✔
294
        }
1,443✔
295

296
        top := ew.blockExpiryQueue.Top().(*invoiceExpiryHeight)
380✔
297
        if !top.expired(ew.currentHeight, ew.blockExpiryDelta) {
740✔
298
                return nil
360✔
299
        }
360✔
300

301
        blockChan := make(chan uint32, 1)
20✔
302
        blockChan <- top.expiryHeight
20✔
303
        return blockChan
20✔
304
}
305

306
// cancelNextExpiredInvoice will cancel the next expired invoice and removes
307
// it from the expiry queue.
308
func (ew *InvoiceExpiryWatcher) cancelNextExpiredInvoice() {
1,831✔
309
        if !ew.timestampExpiryQueue.Empty() {
2,981✔
310
                top := ew.timestampExpiryQueue.Top().(*invoiceExpiryTs)
1,150✔
311
                if !top.Expiry.Before(ew.clock.Now()) {
2,219✔
312
                        return
1,069✔
313
                }
1,069✔
314

315
                // Don't force-cancel already accepted invoices. An exception to
316
                // this are auto-generated keysend invoices. Because those move
317
                // to the Accepted state directly after being opened, the expiry
318
                // field would never be used. Enabling cancellation for accepted
319
                // keysend invoices creates a safety mechanism that can prevents
320
                // channel force-closes.
321
                ew.expireInvoice(top.PaymentHash, top.Keysend)
81✔
322
                ew.timestampExpiryQueue.Pop()
81✔
323
        }
324
}
325

326
// cancelNextHeightExpiredInvoice looks at our height based queue and expires
327
// the next invoice if we have reached its expiry block.
328
func (ew *InvoiceExpiryWatcher) cancelNextHeightExpiredInvoice() {
15✔
329
        if ew.blockExpiryQueue.Empty() {
15✔
UNCOV
330
                return
×
UNCOV
331
        }
×
332

333
        top := ew.blockExpiryQueue.Top().(*invoiceExpiryHeight)
15✔
334
        if !top.expired(ew.currentHeight, ew.blockExpiryDelta) {
15✔
335
                return
×
336
        }
×
337

338
        // We always force-cancel block-based expiry so that we can
339
        // cancel invoices that have been accepted but not yet resolved.
340
        // This helps us avoid force closes.
341
        ew.expireInvoice(top.paymentHash, true)
15✔
342
        ew.blockExpiryQueue.Pop()
15✔
343
}
344

345
// expireInvoice attempts to expire an invoice and logs an error if we get an
346
// unexpected error.
347
func (ew *InvoiceExpiryWatcher) expireInvoice(hash lntypes.Hash, force bool) {
96✔
348
        err := ew.cancelInvoice(hash, force)
96✔
349
        switch {
96✔
350
        case err == nil:
92✔
351

352
        case errors.Is(err, ErrInvoiceAlreadyCanceled):
×
353

354
        case errors.Is(err, ErrInvoiceAlreadySettled):
4✔
355

356
        case errors.Is(err, ErrInvoiceNotFound):
×
357
                // It's possible that the user has manually canceled the invoice
358
                // which will then be deleted by the garbage collector resulting
359
                // in an ErrInvoiceNotFound error.
360

361
        default:
×
362
                log.Errorf("Unable to cancel invoice: %v: %v", hash, err)
×
363
        }
364
}
365

366
// pushInvoices adds invoices to be expired to their relevant queue.
367
func (ew *InvoiceExpiryWatcher) pushInvoices(invoices []invoiceExpiry) {
822✔
368
        for _, inv := range invoices {
1,680✔
369
                // Switch on the type of entry we have. We need to check nil
858✔
370
                // on the implementation of the interface because the interface
858✔
371
                // itself is non-nil.
858✔
372
                switch expiry := inv.(type) {
858✔
373
                case *invoiceExpiryTs:
772✔
374
                        if expiry != nil {
1,544✔
375
                                ew.timestampExpiryQueue.Push(expiry)
772✔
376
                        }
772✔
377

378
                case *invoiceExpiryHeight:
86✔
379
                        if expiry != nil {
171✔
380
                                ew.blockExpiryQueue.Push(expiry)
85✔
381
                        }
85✔
382

383
                default:
×
384
                        log.Errorf("unexpected queue item: %T", inv)
×
385
                }
386
        }
387
}
388

389
// mainLoop is a goroutine that receives new invoices and handles cancellation
390
// of expired invoices.
391
func (ew *InvoiceExpiryWatcher) mainLoop(blockNtfns *chainntnfs.BlockEpochEvent) {
655✔
392
        defer func() {
1,049✔
393
                blockNtfns.Cancel()
394✔
394
                ew.wg.Done()
394✔
395
        }()
394✔
396

397
        // We have two different queues, so we use a different cancel method
398
        // depending on which expiry condition we have hit. Starting with time
399
        // based expiry is an arbitrary choice to start off.
400
        cancelNext := ew.cancelNextExpiredInvoice
655✔
401

655✔
402
        for {
2,501✔
403
                // Cancel any invoices that may have expired.
1,846✔
404
                cancelNext()
1,846✔
405

1,846✔
406
                select {
1,846✔
407
                case newInvoices := <-ew.newInvoices:
23✔
408
                        // Take newly forwarded invoices with higher priority
23✔
409
                        // in order to not block the newInvoices channel.
23✔
410
                        ew.pushInvoices(newInvoices)
23✔
411
                        continue
23✔
412

413
                default:
1,823✔
414
                        select {
1,823✔
415
                        // Wait until the next invoice expires.
416
                        case <-ew.nextTimestampExpiry():
322✔
417
                                cancelNext = ew.cancelNextExpiredInvoice
322✔
418
                                continue
322✔
419

420
                        case <-ew.nextHeightExpiry():
15✔
421
                                cancelNext = ew.cancelNextHeightExpiredInvoice
15✔
422
                                continue
15✔
423

424
                        case newInvoices := <-ew.newInvoices:
799✔
425
                                ew.pushInvoices(newInvoices)
799✔
426

427
                        // Consume new blocks.
428
                        case block, ok := <-blockNtfns.Epochs:
32✔
429
                                if !ok {
32✔
430
                                        log.Debugf("block notifications " +
×
431
                                                "canceled")
×
432
                                        return
×
433
                                }
×
434

435
                                ew.currentHeight = uint32(block.Height)
32✔
436
                                ew.currentHash = block.Hash
32✔
437

438
                        case <-ew.quit:
394✔
439
                                return
394✔
440
                        }
441
                }
442
        }
443
}
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