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

lightningnetwork / lnd / 12583319996

02 Jan 2025 01:38PM UTC coverage: 57.522% (-1.1%) from 58.598%
12583319996

Pull #9361

github

starius
fn/ContextGuard: use context.AfterFunc to wait

Simplifies context cancellation handling by using context.AfterFunc instead of a
goroutine to wait for context cancellation. This approach avoids the overhead of
a goroutine during the waiting period.

For ctxQuitUnsafe, since g.quit is closed only in the Quit method (which also
cancels all associated contexts), waiting on context cancellation ensures the
same behavior without unnecessary dependency on g.quit.

Added a test to ensure that the Create method does not launch any goroutines.
Pull Request #9361: fn: optimize context guard

102587 of 178344 relevant lines covered (57.52%)

24734.33 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 {
546✔
36
        return e.Expiry.Before(other.(*invoiceExpiryTs).Expiry)
546✔
37
}
546✔
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 {
305✔
58
        return currentHeight+delta >= b.expiryHeight
305✔
59
}
305✔
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 {
645✔
121

645✔
122
        return &InvoiceExpiryWatcher{
645✔
123
                clock:            clock,
645✔
124
                notifier:         notifier,
645✔
125
                blockExpiryDelta: expiryDelta,
645✔
126
                currentHeight:    startHeight,
645✔
127
                currentHash:      startHash,
645✔
128
                newInvoices:      make(chan []invoiceExpiry),
645✔
129
                quit:             make(chan struct{}),
645✔
130
        }
645✔
131
}
645✔
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 {
647✔
139

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

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

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

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

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

646✔
161
        return nil
646✔
162
}
163

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

384✔
170
        if ew.started {
768✔
171
                // Signal subscriptionHandler to quit and wait for it to return.
384✔
172
                close(ew.quit)
384✔
173
                ew.wg.Wait()
384✔
174
                ew.started = false
384✔
175
        }
384✔
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 {
846✔
182

846✔
183
        switch invoice.State {
846✔
184
        // If we have an open invoice with no htlcs, we want to expire the
185
        // invoice based on timestamp
186
        case ContractOpen:
762✔
187
                return makeTimestampExpiry(paymentHash, invoice)
762✔
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 {
762✔
226

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

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

236
        expiry := invoice.CreationDate.Add(realExpiry)
762✔
237
        return &invoiceExpiryTs{
762✔
238
                PaymentHash: paymentHash,
762✔
239
                Expiry:      expiry,
762✔
240
                Keysend:     len(invoice.PaymentRequest) == 0,
762✔
241
        }
762✔
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,446✔
262
        if len(invoices) == 0 {
2,079✔
263
                return
633✔
264
        }
633✔
265

266
        select {
813✔
267
        case ew.newInvoices <- invoices:
813✔
268
                log.Debugf("Added %d invoices to the expiry watcher",
813✔
269
                        len(invoices))
813✔
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,713✔
281
        if !ew.timestampExpiryQueue.Empty() {
2,725✔
282
                top := ew.timestampExpiryQueue.Top().(*invoiceExpiryTs)
1,012✔
283
                return ew.clock.TickAfter(top.Expiry.Sub(ew.clock.Now()))
1,012✔
284
        }
1,012✔
285

286
        return nil
701✔
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,713✔
292
        if ew.blockExpiryQueue.Empty() {
3,138✔
293
                return nil
1,425✔
294
        }
1,425✔
295

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

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

306
// cancelNextExpiredInvoice will cancel the next expired invoice and removes
307
// it from the expiry queue.
308
func (ew *InvoiceExpiryWatcher) cancelNextExpiredInvoice() {
1,724✔
309
        if !ew.timestampExpiryQueue.Empty() {
2,776✔
310
                top := ew.timestampExpiryQueue.Top().(*invoiceExpiryTs)
1,052✔
311
                if !top.Expiry.Before(ew.clock.Now()) {
2,032✔
312
                        return
980✔
313
                }
980✔
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)
72✔
322
                ew.timestampExpiryQueue.Pop()
72✔
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() {
17✔
329
        if ew.blockExpiryQueue.Empty() {
17✔
330
                return
×
331
        }
×
332

333
        top := ew.blockExpiryQueue.Top().(*invoiceExpiryHeight)
17✔
334
        if !top.expired(ew.currentHeight, ew.blockExpiryDelta) {
17✔
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)
17✔
342
        ew.blockExpiryQueue.Pop()
17✔
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) {
89✔
348
        err := ew.cancelInvoice(hash, force)
89✔
349
        switch {
89✔
350
        case err == nil:
83✔
351

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

354
        case errors.Is(err, ErrInvoiceAlreadySettled):
6✔
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) {
813✔
368
        for _, inv := range invoices {
1,662✔
369
                // Switch on the type of entry we have. We need to check nil
849✔
370
                // on the implementation of the interface because the interface
849✔
371
                // itself is non-nil.
849✔
372
                switch expiry := inv.(type) {
849✔
373
                case *invoiceExpiryTs:
763✔
374
                        if expiry != nil {
1,526✔
375
                                ew.timestampExpiryQueue.Push(expiry)
763✔
376
                        }
763✔
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) {
646✔
392
        defer func() {
1,031✔
393
                blockNtfns.Cancel()
385✔
394
                ew.wg.Done()
385✔
395
        }()
385✔
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
646✔
401

646✔
402
        for {
2,387✔
403
                // Cancel any invoices that may have expired.
1,741✔
404
                cancelNext()
1,741✔
405

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

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

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

424
                        case newInvoices := <-ew.newInvoices:
785✔
425
                                ew.pushInvoices(newInvoices)
785✔
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:
385✔
439
                                return
385✔
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