invoice_expiry_watcher_test.go
1 package invoices 2 3 import ( 4 "sync" 5 "testing" 6 "time" 7 8 "github.com/lightningnetwork/lnd/clock" 9 "github.com/lightningnetwork/lnd/lntypes" 10 "github.com/stretchr/testify/require" 11 ) 12 13 // invoiceExpiryWatcherTest holds a test fixture and implements checks 14 // for InvoiceExpiryWatcher tests. 15 type invoiceExpiryWatcherTest struct { 16 t *testing.T 17 wg sync.WaitGroup 18 watcher *InvoiceExpiryWatcher 19 testData invoiceExpiryTestData 20 canceledInvoices []lntypes.Hash 21 } 22 23 // newInvoiceExpiryWatcherTest creates a new InvoiceExpiryWatcher test fixture 24 // and sets up the test environment. 25 func newInvoiceExpiryWatcherTest(t *testing.T, now time.Time, 26 numExpiredInvoices, numPendingInvoices int) *invoiceExpiryWatcherTest { 27 28 mockNotifier := newMockNotifier() 29 test := &invoiceExpiryWatcherTest{ 30 watcher: NewInvoiceExpiryWatcher( 31 clock.NewTestClock(testTime), 0, 32 uint32(testCurrentHeight), nil, mockNotifier, 33 ), 34 testData: generateInvoiceExpiryTestData( 35 t, now, 0, numExpiredInvoices, numPendingInvoices, 36 ), 37 } 38 39 test.wg.Add(numExpiredInvoices) 40 41 err := test.watcher.Start(func(paymentHash lntypes.Hash, 42 force bool) error { 43 44 test.canceledInvoices = append( 45 test.canceledInvoices, paymentHash, 46 ) 47 test.wg.Done() 48 return nil 49 }) 50 51 require.NoError(t, err, "cannot start InvoiceExpiryWatcher") 52 53 return test 54 } 55 56 func (t *invoiceExpiryWatcherTest) waitForFinish(timeout time.Duration) { 57 done := make(chan struct{}) 58 59 // Wait for all cancels. 60 go func() { 61 t.wg.Wait() 62 close(done) 63 }() 64 65 select { 66 case <-done: 67 case <-time.After(timeout): 68 t.t.Fatalf("test timeout") 69 } 70 } 71 72 func (t *invoiceExpiryWatcherTest) checkExpectations() { 73 // Check that invoices that got canceled during the test are the ones 74 // that expired. 75 if len(t.canceledInvoices) != len(t.testData.expiredInvoices) { 76 t.t.Fatalf("expected %v cancellations, got %v", 77 len(t.testData.expiredInvoices), 78 len(t.canceledInvoices)) 79 } 80 81 for i := range t.canceledInvoices { 82 if _, ok := t.testData.expiredInvoices[t.canceledInvoices[i]]; !ok { 83 t.t.Fatalf("wrong invoice canceled") 84 } 85 } 86 } 87 88 // Tests that InvoiceExpiryWatcher can be started and stopped. 89 func TestInvoiceExpiryWatcherStartStop(t *testing.T) { 90 watcher := NewInvoiceExpiryWatcher( 91 clock.NewTestClock(testTime), 0, uint32(testCurrentHeight), nil, 92 newMockNotifier(), 93 ) 94 cancel := func(lntypes.Hash, bool) error { 95 t.Fatalf("unexpected call") 96 return nil 97 } 98 99 if err := watcher.Start(cancel); err != nil { 100 t.Fatalf("unexpected error upon start: %v", err) 101 } 102 103 if err := watcher.Start(cancel); err == nil { 104 t.Fatalf("expected error upon second start") 105 } 106 107 watcher.Stop() 108 109 if err := watcher.Start(cancel); err != nil { 110 t.Fatalf("unexpected error upon start: %v", err) 111 } 112 } 113 114 // Tests that no invoices will expire from an empty InvoiceExpiryWatcher. 115 func TestInvoiceExpiryWithNoInvoices(t *testing.T) { 116 t.Parallel() 117 118 test := newInvoiceExpiryWatcherTest(t, testTime, 0, 0) 119 120 test.waitForFinish(testTimeout) 121 test.watcher.Stop() 122 test.checkExpectations() 123 } 124 125 // Tests that if all add invoices are expired, then all invoices 126 // will be canceled. 127 func TestInvoiceExpiryWithOnlyExpiredInvoices(t *testing.T) { 128 t.Parallel() 129 130 test := newInvoiceExpiryWatcherTest(t, testTime, 0, 5) 131 132 for paymentHash, invoice := range test.testData.pendingInvoices { 133 test.watcher.AddInvoices(makeInvoiceExpiry(paymentHash, invoice)) 134 } 135 136 test.waitForFinish(testTimeout) 137 test.watcher.Stop() 138 test.checkExpectations() 139 } 140 141 // Tests that if some invoices are expired, then those invoices 142 // will be canceled. 143 func TestInvoiceExpiryWithPendingAndExpiredInvoices(t *testing.T) { 144 t.Parallel() 145 146 test := newInvoiceExpiryWatcherTest(t, testTime, 5, 5) 147 148 for paymentHash, invoice := range test.testData.expiredInvoices { 149 test.watcher.AddInvoices(makeInvoiceExpiry(paymentHash, invoice)) 150 } 151 152 for paymentHash, invoice := range test.testData.pendingInvoices { 153 test.watcher.AddInvoices(makeInvoiceExpiry(paymentHash, invoice)) 154 } 155 156 test.waitForFinish(testTimeout) 157 test.watcher.Stop() 158 test.checkExpectations() 159 } 160 161 // Tests adding multiple invoices at once. 162 func TestInvoiceExpiryWhenAddingMultipleInvoices(t *testing.T) { 163 t.Parallel() 164 165 test := newInvoiceExpiryWatcherTest(t, testTime, 5, 5) 166 var invoices []invoiceExpiry 167 168 for hash, invoice := range test.testData.expiredInvoices { 169 invoices = append(invoices, makeInvoiceExpiry(hash, invoice)) 170 } 171 172 for hash, invoice := range test.testData.pendingInvoices { 173 invoices = append(invoices, makeInvoiceExpiry(hash, invoice)) 174 } 175 176 test.watcher.AddInvoices(invoices...) 177 test.waitForFinish(testTimeout) 178 test.watcher.Stop() 179 test.checkExpectations() 180 } 181 182 // TestExpiredHodlInv tests expiration of an already-expired hodl invoice 183 // which has no htlcs. 184 func TestExpiredHodlInv(t *testing.T) { 185 t.Parallel() 186 187 creationDate := testTime.Add(time.Hour * -24) 188 expiry := time.Hour 189 190 test := setupHodlExpiry( 191 t, creationDate, expiry, 0, ContractOpen, nil, 192 ) 193 194 test.assertCanceled(t, test.hash) 195 test.watcher.Stop() 196 } 197 198 // TestAcceptedHodlNotExpired tests that hodl invoices which are in an accepted 199 // state are not expired once their time-based expiry elapses, using a regular 200 // invoice that expires at the same time as a control to ensure that invoices 201 // with that timestamp would otherwise be expired. 202 func TestAcceptedHodlNotExpired(t *testing.T) { 203 t.Parallel() 204 205 creationDate := testTime 206 expiry := time.Hour 207 208 test := setupHodlExpiry( 209 t, creationDate, expiry, 0, ContractAccepted, nil, 210 ) 211 defer test.watcher.Stop() 212 213 // Add another invoice that will expire at our expiry time as a control 214 // value. 215 tsExpires := &invoiceExpiryTs{ 216 PaymentHash: lntypes.Hash{1, 2, 3}, 217 Expiry: creationDate.Add(expiry), 218 Keysend: true, 219 } 220 test.watcher.AddInvoices(tsExpires) 221 222 test.mockClock.SetTime(creationDate.Add(expiry + 1)) 223 224 // Assert that only the ts expiry invoice is expired. 225 test.assertCanceled(t, tsExpires.PaymentHash) 226 } 227 228 // TestHeightAlreadyExpired tests the case where we add an invoice with htlcs 229 // that have already expired to the expiry watcher. 230 func TestHeightAlreadyExpired(t *testing.T) { 231 t.Parallel() 232 233 expiredHtlc := []*InvoiceHTLC{ 234 { 235 State: HtlcStateAccepted, 236 Expiry: uint32(testCurrentHeight), 237 }, 238 } 239 240 test := setupHodlExpiry( 241 t, testTime, time.Hour, 0, ContractAccepted, 242 expiredHtlc, 243 ) 244 defer test.watcher.Stop() 245 246 test.assertCanceled(t, test.hash) 247 } 248 249 // TestExpiryHeightArrives tests the case where we add a hodl invoice to the 250 // expiry watcher when it has no htlcs, htlcs are added and then they finally 251 // expire. We use a non-zero delta for this test to check that we expire with 252 // sufficient buffer. 253 func TestExpiryHeightArrives(t *testing.T) { 254 var ( 255 creationDate = testTime 256 expiry = time.Hour * 2 257 delta uint32 = 1 258 ) 259 260 // Start out with a hodl invoice that is open, and has no htlcs. 261 test := setupHodlExpiry( 262 t, creationDate, expiry, delta, ContractOpen, nil, 263 ) 264 defer test.watcher.Stop() 265 266 htlc1 := uint32(testCurrentHeight + 10) 267 expiry1 := makeHeightExpiry(test.hash, htlc1) 268 269 // Add htlcs to our invoice and progress its state to accepted. 270 test.watcher.AddInvoices(expiry1) 271 test.setState(ContractAccepted) 272 273 // Progress time so that our expiry has elapsed. We no longer expect 274 // this invoice to be canceled because it has been accepted. 275 test.mockClock.SetTime(creationDate.Add(expiry)) 276 277 // Tick our mock block subscription with the next block, we don't 278 // expect anything to happen. 279 currentHeight := uint32(testCurrentHeight + 1) 280 test.announceBlock(t, currentHeight) 281 282 // Now, we add another htlc to the invoice. This one has a lower expiry 283 // height than our current ones. 284 htlc2 := currentHeight + 5 285 expiry2 := makeHeightExpiry(test.hash, htlc2) 286 test.watcher.AddInvoices(expiry2) 287 288 // Announce our lowest htlc expiry block minus our delta, the invoice 289 // should be expired now. 290 test.announceBlock(t, htlc2-delta) 291 test.assertCanceled(t, test.hash) 292 }