34de5922ed
This commit adds height-based invoice expiry for hodl invoices that have active htlcs. This allows us to cancel our intentionally held htlcs before channels are force closed. We only add this for hodl invoices because we expect regular invoices to automatically be resolved. We still keep hodl invoices in the time-based expiry queue, because we want to expire open invoices that reach their timeout before any htlcs are added. Since htlcs are added after the invoice is created, we add new htlcs as they arrive in the invoice registry. In this commit, we allow adding of duplicate entries for an invoice to be added to the expiry queue as each htlc arrives to keep implementation simple. Our cancellation logic can already handle the case where an entry is already canceled, so this is ok.
370 lines
8.7 KiB
Go
370 lines
8.7 KiB
Go
package invoices
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"runtime/pprof"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockPayload struct {
|
|
mpp *record.MPP
|
|
amp *record.AMP
|
|
customRecords record.CustomSet
|
|
}
|
|
|
|
func (p *mockPayload) MultiPath() *record.MPP {
|
|
return p.mpp
|
|
}
|
|
|
|
func (p *mockPayload) AMPRecord() *record.AMP {
|
|
return p.amp
|
|
}
|
|
|
|
func (p *mockPayload) CustomRecords() record.CustomSet {
|
|
// This function should always return a map instance, but for mock
|
|
// configuration we do accept nil.
|
|
if p.customRecords == nil {
|
|
return make(record.CustomSet)
|
|
}
|
|
|
|
return p.customRecords
|
|
}
|
|
|
|
const (
|
|
testHtlcExpiry = uint32(5)
|
|
|
|
testInvoiceCltvDelta = uint32(4)
|
|
|
|
testFinalCltvRejectDelta = int32(4)
|
|
|
|
testCurrentHeight = int32(1)
|
|
)
|
|
|
|
var (
|
|
testTimeout = 5 * time.Second
|
|
|
|
testTime = time.Date(2018, time.February, 2, 14, 0, 0, 0, time.UTC)
|
|
|
|
testInvoicePreimage = lntypes.Preimage{1}
|
|
|
|
testInvoicePaymentHash = testInvoicePreimage.Hash()
|
|
|
|
testPrivKeyBytes, _ = hex.DecodeString(
|
|
"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
|
|
|
|
testPrivKey, _ = btcec.PrivKeyFromBytes(
|
|
btcec.S256(), testPrivKeyBytes)
|
|
|
|
testInvoiceDescription = "coffee"
|
|
|
|
testInvoiceAmount = lnwire.MilliSatoshi(100000)
|
|
|
|
testNetParams = &chaincfg.MainNetParams
|
|
|
|
testMessageSigner = zpay32.MessageSigner{
|
|
SignCompact: func(hash []byte) ([]byte, error) {
|
|
sig, err := btcec.SignCompact(btcec.S256(), 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,
|
|
)
|
|
|
|
testPayload = &mockPayload{}
|
|
|
|
testInvoiceCreationDate = testTime
|
|
)
|
|
|
|
var (
|
|
testInvoiceAmt = lnwire.MilliSatoshi(100000)
|
|
testInvoice = &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
PaymentPreimage: &testInvoicePreimage,
|
|
Value: testInvoiceAmt,
|
|
Expiry: time.Hour,
|
|
Features: testFeatures,
|
|
},
|
|
CreationDate: testInvoiceCreationDate,
|
|
}
|
|
|
|
testPayAddrReqInvoice = &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
PaymentPreimage: &testInvoicePreimage,
|
|
Value: testInvoiceAmt,
|
|
Expiry: time.Hour,
|
|
Features: lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(
|
|
lnwire.TLVOnionPayloadOptional,
|
|
lnwire.PaymentAddrRequired,
|
|
),
|
|
lnwire.Features,
|
|
),
|
|
},
|
|
CreationDate: testInvoiceCreationDate,
|
|
}
|
|
|
|
testPayAddrOptionalInvoice = &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
PaymentPreimage: &testInvoicePreimage,
|
|
Value: testInvoiceAmt,
|
|
Expiry: time.Hour,
|
|
Features: lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(
|
|
lnwire.TLVOnionPayloadOptional,
|
|
lnwire.PaymentAddrOptional,
|
|
),
|
|
lnwire.Features,
|
|
),
|
|
},
|
|
CreationDate: testInvoiceCreationDate,
|
|
}
|
|
|
|
testHodlInvoice = &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
Value: testInvoiceAmt,
|
|
Expiry: time.Hour,
|
|
Features: testFeatures,
|
|
},
|
|
CreationDate: testInvoiceCreationDate,
|
|
HodlInvoice: true,
|
|
}
|
|
)
|
|
|
|
func newTestChannelDB(clock clock.Clock) (*channeldb.DB, func(), error) {
|
|
// First, create a temporary directory to be used for the duration of
|
|
// this test.
|
|
tempDirName, err := ioutil.TempDir("", "channeldb")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Next, create channeldb for the first time.
|
|
cdb, err := channeldb.Open(
|
|
tempDirName, channeldb.OptionClock(clock),
|
|
)
|
|
if err != nil {
|
|
os.RemoveAll(tempDirName)
|
|
return nil, nil, err
|
|
}
|
|
|
|
cleanUp := func() {
|
|
cdb.Close()
|
|
os.RemoveAll(tempDirName)
|
|
}
|
|
|
|
return cdb, cleanUp, nil
|
|
}
|
|
|
|
type testContext struct {
|
|
cdb *channeldb.DB
|
|
registry *InvoiceRegistry
|
|
clock *clock.TestClock
|
|
|
|
cleanup func()
|
|
t *testing.T
|
|
}
|
|
|
|
func newTestContext(t *testing.T) *testContext {
|
|
clock := clock.NewTestClock(testTime)
|
|
|
|
cdb, cleanup, err := newTestChannelDB(clock)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expiryWatcher := NewInvoiceExpiryWatcher(
|
|
clock, 0, uint32(testCurrentHeight), nil, newMockNotifier(),
|
|
)
|
|
|
|
// Instantiate and start the invoice ctx.registry.
|
|
cfg := RegistryConfig{
|
|
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
|
HtlcHoldDuration: 30 * time.Second,
|
|
Clock: clock,
|
|
}
|
|
registry := NewRegistry(cdb, expiryWatcher, &cfg)
|
|
|
|
err = registry.Start()
|
|
if err != nil {
|
|
cleanup()
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := testContext{
|
|
cdb: cdb,
|
|
registry: registry,
|
|
clock: clock,
|
|
t: t,
|
|
cleanup: func() {
|
|
if err = registry.Stop(); err != nil {
|
|
t.Fatalf("failed to stop invoice registry: %v", err)
|
|
}
|
|
cleanup()
|
|
},
|
|
}
|
|
|
|
return &ctx
|
|
}
|
|
|
|
func getCircuitKey(htlcID uint64) channeldb.CircuitKey {
|
|
return channeldb.CircuitKey{
|
|
ChanID: lnwire.ShortChannelID{
|
|
BlockHeight: 1, TxIndex: 2, TxPosition: 3,
|
|
},
|
|
HtlcID: htlcID,
|
|
}
|
|
}
|
|
|
|
func newTestInvoice(t *testing.T, preimage lntypes.Preimage,
|
|
timestamp time.Time, expiry time.Duration) *channeldb.Invoice {
|
|
|
|
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),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Error while creating new invoice: %v", err)
|
|
}
|
|
|
|
paymentRequest, err := rawInvoice.Encode(testMessageSigner)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error while encoding payment request: %v", err)
|
|
}
|
|
|
|
return &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
PaymentPreimage: &preimage,
|
|
PaymentAddr: payAddr,
|
|
Value: testInvoiceAmount,
|
|
Expiry: expiry,
|
|
Features: testFeatures,
|
|
},
|
|
PaymentRequest: []byte(paymentRequest),
|
|
CreationDate: timestamp,
|
|
}
|
|
}
|
|
|
|
// timeout implements a test level timeout.
|
|
func timeout() func() {
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
err := pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("error writing to std out after timeout: %v", err))
|
|
}
|
|
panic("timeout")
|
|
case <-done:
|
|
}
|
|
}()
|
|
|
|
return func() {
|
|
close(done)
|
|
}
|
|
}
|
|
|
|
// invoiceExpiryTestData simply holds generated expired and pending invoices.
|
|
type invoiceExpiryTestData struct {
|
|
expiredInvoices map[lntypes.Hash]*channeldb.Invoice
|
|
pendingInvoices map[lntypes.Hash]*channeldb.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 {
|
|
|
|
var testData invoiceExpiryTestData
|
|
|
|
testData.expiredInvoices = make(map[lntypes.Hash]*channeldb.Invoice)
|
|
testData.pendingInvoices = make(map[lntypes.Hash]*channeldb.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))
|
|
expiry := time.Duration((i+offset)%24) * 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))
|
|
expiry := time.Duration((i+offset)%24) * time.Hour
|
|
invoice := newTestInvoice(t, preimage, now, expiry)
|
|
testData.pendingInvoices[preimage.Hash()] = invoice
|
|
}
|
|
|
|
return testData
|
|
}
|
|
|
|
// checkSettleResolution asserts the resolution is a settle with the correct
|
|
// preimage. If successful, the HtlcSettleResolution is returned in case further
|
|
// checks are desired.
|
|
func checkSettleResolution(t *testing.T, res HtlcResolution,
|
|
expPreimage lntypes.Preimage) *HtlcSettleResolution {
|
|
|
|
t.Helper()
|
|
|
|
settleResolution, ok := res.(*HtlcSettleResolution)
|
|
require.True(t, ok)
|
|
require.Equal(t, expPreimage, settleResolution.Preimage)
|
|
|
|
return settleResolution
|
|
}
|
|
|
|
// checkFailResolution asserts the resolution is a fail with the correct reason.
|
|
// If successful, the HtlcFailResolutionis returned in case further checks are
|
|
// desired.
|
|
func checkFailResolution(t *testing.T, res HtlcResolution,
|
|
expOutcome FailResolutionResult) *HtlcFailResolution {
|
|
|
|
t.Helper()
|
|
failResolution, ok := res.(*HtlcFailResolution)
|
|
require.True(t, ok)
|
|
require.Equal(t, expOutcome, failResolution.Outcome)
|
|
|
|
return failResolution
|
|
}
|