/ invoices / invoice_expiry_watcher_test.go
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  }