package invoices import ( "crypto/rand" "encoding/binary" "encoding/hex" "fmt" "sync" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/zpay32" "github.com/stretchr/testify/require" ) type mockChainNotifier struct { chainntnfs.ChainNotifier blockChan chan *chainntnfs.BlockEpoch } func newMockNotifier() *mockChainNotifier { return &mockChainNotifier{ blockChan: make(chan *chainntnfs.BlockEpoch), } } // RegisterBlockEpochNtfn mocks a block epoch notification, using the mock's // block channel to deliver blocks to the client. func (m *mockChainNotifier) RegisterBlockEpochNtfn(*chainntnfs.BlockEpoch) ( *chainntnfs.BlockEpochEvent, error) { return &chainntnfs.BlockEpochEvent{ Epochs: m.blockChan, Cancel: func() {}, }, nil } const ( testCurrentHeight = int32(1) ) var ( testTimeout = 5 * time.Second testTime = time.Date(2018, time.February, 2, 14, 0, 0, 0, time.UTC) testPrivKeyBytes, _ = hex.DecodeString( "e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2d" + "b734", ) testPrivKey, _ = btcec.PrivKeyFromBytes(testPrivKeyBytes) testInvoiceDescription = "coffee" testInvoiceAmount = lnwire.MilliSatoshi(100000) testNetParams = &chaincfg.MainNetParams testMessageSigner = zpay32.MessageSigner{ SignCompact: func(msg []byte) ([]byte, error) { hash := chainhash.HashB(msg) sig, err := ecdsa.SignCompact(testPrivKey, hash, true) if err != nil { return nil, fmt.Errorf("can't sign the "+ "message: %v", err) } return sig, nil }, } testFeatures = lnwire.NewFeatureVector( nil, lnwire.Features, ) ) func newTestInvoice(t *testing.T, preimage lntypes.Preimage, timestamp time.Time, expiry time.Duration) *Invoice { t.Helper() if expiry == 0 { expiry = time.Hour } var payAddr [32]byte if _, err := rand.Read(payAddr[:]); err != nil { t.Fatalf("unable to generate payment addr: %v", err) } rawInvoice, err := zpay32.NewInvoice( testNetParams, preimage.Hash(), timestamp, zpay32.Amount(testInvoiceAmount), zpay32.Description(testInvoiceDescription), zpay32.Expiry(expiry), zpay32.PaymentAddr(payAddr), ) require.NoError(t, err, "Error while creating new invoice") paymentRequest, err := rawInvoice.Encode(testMessageSigner) require.NoError(t, err, "Error while encoding payment request") return &Invoice{ Terms: ContractTerm{ PaymentPreimage: &preimage, PaymentAddr: payAddr, Value: testInvoiceAmount, Expiry: expiry, Features: testFeatures, }, PaymentRequest: []byte(paymentRequest), CreationDate: timestamp, } } // invoiceExpiryTestData simply holds generated expired and pending invoices. type invoiceExpiryTestData struct { expiredInvoices map[lntypes.Hash]*Invoice pendingInvoices map[lntypes.Hash]*Invoice } // generateInvoiceExpiryTestData generates the specified number of fake expired // and pending invoices anchored to the passed now timestamp. func generateInvoiceExpiryTestData( t *testing.T, now time.Time, offset, numExpired, numPending int) invoiceExpiryTestData { t.Helper() var testData invoiceExpiryTestData testData.expiredInvoices = make(map[lntypes.Hash]*Invoice) testData.pendingInvoices = make(map[lntypes.Hash]*Invoice) expiredCreationDate := now.Add(-24 * time.Hour) for i := 1; i <= numExpired; i++ { var preimage lntypes.Preimage binary.BigEndian.PutUint32(preimage[:4], uint32(offset+i)) duration := (i + offset) % 24 expiry := time.Duration(duration) * time.Hour invoice := newTestInvoice( t, preimage, expiredCreationDate, expiry, ) testData.expiredInvoices[preimage.Hash()] = invoice } for i := 1; i <= numPending; i++ { var preimage lntypes.Preimage binary.BigEndian.PutUint32(preimage[4:], uint32(offset+i)) duration := (i + offset) % 24 expiry := time.Duration(duration) * time.Hour invoice := newTestInvoice(t, preimage, now, expiry) testData.pendingInvoices[preimage.Hash()] = invoice } return testData } type hodlExpiryTest struct { hash lntypes.Hash state ContractState stateLock sync.Mutex mockNotifier *mockChainNotifier mockClock *clock.TestClock cancelChan chan lntypes.Hash watcher *InvoiceExpiryWatcher } func (h *hodlExpiryTest) setState(state ContractState) { h.stateLock.Lock() defer h.stateLock.Unlock() h.state = state } func (h *hodlExpiryTest) announceBlock(t *testing.T, height uint32) { t.Helper() select { case h.mockNotifier.blockChan <- &chainntnfs.BlockEpoch{ Height: int32(height), }: case <-time.After(testTimeout): t.Fatalf("block %v not consumed", height) } } func (h *hodlExpiryTest) assertCanceled(t *testing.T, expected lntypes.Hash) { t.Helper() select { case actual := <-h.cancelChan: require.Equal(t, expected, actual) case <-time.After(testTimeout): t.Fatalf("invoice: %v not canceled", h.hash) } } // setupHodlExpiry creates a hodl invoice in our expiry watcher and runs an // arbitrary update function which advances the invoices's state. func setupHodlExpiry(t *testing.T, creationDate time.Time, expiry time.Duration, heightDelta uint32, startState ContractState, startHtlcs []*InvoiceHTLC) *hodlExpiryTest { t.Helper() mockNotifier := newMockNotifier() mockClock := clock.NewTestClock(testTime) test := &hodlExpiryTest{ state: startState, watcher: NewInvoiceExpiryWatcher( mockClock, heightDelta, uint32(testCurrentHeight), nil, mockNotifier, ), cancelChan: make(chan lntypes.Hash), mockNotifier: mockNotifier, mockClock: mockClock, } // Use an unbuffered channel to block on cancel calls so that the test // does not exit before we've processed all the invoices we expect. cancelImpl := func(paymentHash lntypes.Hash, force bool) error { test.stateLock.Lock() currentState := test.state test.stateLock.Unlock() if currentState != ContractOpen && !force { return nil } select { case test.cancelChan <- paymentHash: case <-time.After(testTimeout): } return nil } require.NoError(t, test.watcher.Start(cancelImpl)) // We set preimage and hash so that we can use our existing test // helpers. In practice we would only have the hash, but this does not // affect what we're testing at all. preimage := lntypes.Preimage{1} test.hash = preimage.Hash() invoice := newTestInvoice(t, preimage, creationDate, expiry) invoice.State = startState invoice.HodlInvoice = true invoice.Htlcs = make(map[CircuitKey]*InvoiceHTLC) // If we have any htlcs, add them with unique circult keys. for i, htlc := range startHtlcs { key := CircuitKey{ HtlcID: uint64(i), } invoice.Htlcs[key] = htlc } // Create an expiry entry for our invoice in its starting state. This // mimics adding invoices to the watcher on start. entry := makeInvoiceExpiry(test.hash, invoice) test.watcher.AddInvoices(entry) return test }