invoices/test: add height expiry watcher tests with registry

This commit is contained in:
carla 2021-05-11 08:45:33 +02:00
parent 8066ff7047
commit 74373f26b9
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
2 changed files with 222 additions and 1 deletions

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/lightningnetwork/lnd/amp" "github.com/lightningnetwork/lnd/amp"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
@ -1118,6 +1119,222 @@ func TestOldInvoiceRemovalOnStart(t *testing.T) {
require.Equal(t, expected, response.Invoices) require.Equal(t, expected, response.Invoices)
} }
// TestHeightExpiryWithRegistry tests our height-based invoice expiry for
// invoices paid with single and multiple htlcs, testing the case where the
// invoice is settled before expiry (and thus not canceled), and the case
// where the invoice is expired.
func TestHeightExpiryWithRegistry(t *testing.T) {
t.Run("single shot settled before expiry", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 1, true)
})
t.Run("single shot expires", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 1, false)
})
t.Run("mpp settled before expiry", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 2, true)
})
t.Run("mpp expires", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 2, false)
})
}
func testHeightExpiryWithRegistry(t *testing.T, numParts int, settle bool) {
t.Parallel()
defer timeout()()
ctx := newTestContext(t)
defer ctx.cleanup()
require.Greater(t, numParts, 0, "test requires at least one part")
// Add a hold invoice, we set a non-nil payment request so that this
// invoice is not considered a keysend by the expiry watcher.
invoice := *testInvoice
invoice.HodlInvoice = true
invoice.PaymentRequest = []byte{1, 2, 3}
_, err := ctx.registry.AddInvoice(&invoice, testInvoicePaymentHash)
require.NoError(t, err)
payLoad := testPayload
if numParts > 1 {
payLoad = &mockPayload{
mpp: record.NewMPP(testInvoiceAmt, [32]byte{}),
}
}
htlcAmt := invoice.Terms.Value / lnwire.MilliSatoshi(numParts)
hodlChan := make(chan interface{}, numParts)
for i := 0; i < numParts; i++ {
// We bump our expiry height for each htlc so that we can test
// that the lowest expiry height is used.
expiry := testHtlcExpiry + uint32(i)
resolution, err := ctx.registry.NotifyExitHopHtlc(
testInvoicePaymentHash, htlcAmt, expiry,
testCurrentHeight, getCircuitKey(uint64(i)), hodlChan,
payLoad,
)
require.NoError(t, err)
require.Nil(t, resolution, "did not expect direct resolution")
}
require.Eventually(t, func() bool {
inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
require.NoError(t, err)
return inv.State == channeldb.ContractAccepted
}, time.Second, time.Millisecond*100)
// Now that we've added our htlc(s), we tick our test clock to our
// invoice expiry time. We don't expect the invoice to be canceled
// based on its expiry time now that we have active htlcs.
ctx.clock.SetTime(invoice.CreationDate.Add(invoice.Terms.Expiry + 1))
// The expiry watcher loop takes some time to process the new clock
// time. We mine the block before our expiry height, our mock will block
// until the expiry watcher consumes this height, so we can be sure
// that the expiry loop has run at least once after this block is
// consumed.
ctx.notifier.blockChan <- &chainntnfs.BlockEpoch{
Height: int32(testHtlcExpiry - 1),
}
// If we want to settle our invoice in this test, we do so now.
if settle {
err = ctx.registry.SettleHodlInvoice(testInvoicePreimage)
require.NoError(t, err)
for i := 0; i < numParts; i++ {
htlcResolution := (<-hodlChan).(HtlcResolution)
require.NotNil(t, htlcResolution)
settleResolution := checkSettleResolution(
t, htlcResolution, testInvoicePreimage,
)
require.Equal(t, ResultSettled, settleResolution.Outcome)
}
}
// Now we mine our htlc's expiry height.
ctx.notifier.blockChan <- &chainntnfs.BlockEpoch{
Height: int32(testHtlcExpiry),
}
// If we did not settle the invoice before its expiry, we now expect
// a cancelation.
expectedState := channeldb.ContractSettled
if !settle {
expectedState = channeldb.ContractCanceled
htlcResolution := (<-hodlChan).(HtlcResolution)
require.NotNil(t, htlcResolution)
checkFailResolution(
t, htlcResolution, ResultCanceled,
)
}
// Finally, lookup the invoice and assert that we have the state we
// expect.
inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
require.NoError(t, err)
require.Equal(t, expectedState, inv.State, "expected "+
"hold invoice: %v, got: %v", expectedState, inv.State)
}
// TestMultipleSetHeightExpiry pays a hold invoice with two mpp sets, testing
// that the invoice expiry watcher only uses the expiry height of the second,
// successful set to cancel the invoice, and does not cancel early using the
// expiry height of the first set that was canceled back due to mpp timeout.
func TestMultipleSetHeightExpiry(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newTestContext(t)
defer ctx.cleanup()
// Add a hold invoice.
invoice := *testInvoice
invoice.HodlInvoice = true
_, err := ctx.registry.AddInvoice(&invoice, testInvoicePaymentHash)
require.NoError(t, err)
mppPayload := &mockPayload{
mpp: record.NewMPP(testInvoiceAmt, [32]byte{}),
}
// Send htlc 1.
hodlChan1 := make(chan interface{}, 1)
resolution, err := ctx.registry.NotifyExitHopHtlc(
testInvoicePaymentHash, invoice.Terms.Value/2,
testHtlcExpiry,
testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
)
require.NoError(t, err)
require.Nil(t, resolution, "did not expect direct resolution")
// Simulate mpp timeout releasing htlc 1.
ctx.clock.SetTime(testTime.Add(30 * time.Second))
htlcResolution := (<-hodlChan1).(HtlcResolution)
failResolution, ok := htlcResolution.(*HtlcFailResolution)
require.True(t, ok, "expected fail resolution, got: %T", resolution)
require.Equal(t, ResultMppTimeout, failResolution.Outcome,
"expected MPP Timeout, got: %v", failResolution.Outcome)
// Notify the expiry height for our first htlc. We don't expect the
// invoice to be expired based on block height because the htlc set
// was never completed.
ctx.notifier.blockChan <- &chainntnfs.BlockEpoch{
Height: int32(testHtlcExpiry),
}
// Now we will send a full set of htlcs for the invoice with a higher
// expiry height. We expect the invoice to move into the accepted state.
expiry := testHtlcExpiry + 5
// Send htlc 2.
hodlChan2 := make(chan interface{}, 1)
resolution, err = ctx.registry.NotifyExitHopHtlc(
testInvoicePaymentHash, invoice.Terms.Value/2, expiry,
testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
)
require.NoError(t, err)
require.Nil(t, resolution, "did not expect direct resolution")
// Send htlc 3.
hodlChan3 := make(chan interface{}, 1)
resolution, err = ctx.registry.NotifyExitHopHtlc(
testInvoicePaymentHash, invoice.Terms.Value/2, expiry,
testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
)
require.NoError(t, err)
require.Nil(t, resolution, "did not expect direct resolution")
// Assert that we've reached an accepted state because the invoice has
// been paid with a complete set.
inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
require.NoError(t, err)
require.Equal(t, channeldb.ContractAccepted, inv.State, "expected "+
"hold invoice accepted")
// Now we will notify the expiry height for the new set of htlcs. We
// expect the invoice to be canceled by the expiry watcher.
ctx.notifier.blockChan <- &chainntnfs.BlockEpoch{
Height: int32(expiry),
}
require.Eventuallyf(t, func() bool {
inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
require.NoError(t, err)
return inv.State == channeldb.ContractCanceled
}, testTimeout, time.Millisecond*100, "invoice not canceled")
}
// TestSettleInvoicePaymentAddrRequired tests that if an incoming payment has // TestSettleInvoicePaymentAddrRequired tests that if an incoming payment has
// an invoice that requires the payment addr bit to be set, and the incoming // an invoice that requires the payment addr bit to be set, and the incoming
// payment doesn't include an mpp payload, then the payment is rejected. // payment doesn't include an mpp payload, then the payment is rejected.

@ -181,6 +181,7 @@ func newTestChannelDB(clock clock.Clock) (*channeldb.DB, func(), error) {
type testContext struct { type testContext struct {
cdb *channeldb.DB cdb *channeldb.DB
registry *InvoiceRegistry registry *InvoiceRegistry
notifier *mockChainNotifier
clock *clock.TestClock clock *clock.TestClock
cleanup func() cleanup func()
@ -195,8 +196,10 @@ func newTestContext(t *testing.T) *testContext {
t.Fatal(err) t.Fatal(err)
} }
notifier := newMockNotifier()
expiryWatcher := NewInvoiceExpiryWatcher( expiryWatcher := NewInvoiceExpiryWatcher(
clock, 0, uint32(testCurrentHeight), nil, newMockNotifier(), clock, 0, uint32(testCurrentHeight), nil, notifier,
) )
// Instantiate and start the invoice ctx.registry. // Instantiate and start the invoice ctx.registry.
@ -216,6 +219,7 @@ func newTestContext(t *testing.T) *testContext {
ctx := testContext{ ctx := testContext{
cdb: cdb, cdb: cdb,
registry: registry, registry: registry,
notifier: notifier,
clock: clock, clock: clock,
t: t, t: t,
cleanup: func() { cleanup: func() {