mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
286 lines
7.0 KiB
Go
286 lines
7.0 KiB
Go
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
|
|
}
|