invoices/test: add height expiry watcher tests with registry
This commit is contained in:
parent
8066ff7047
commit
74373f26b9
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/amp"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
@ -1118,6 +1119,222 @@ func TestOldInvoiceRemovalOnStart(t *testing.T) {
|
||||
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
|
||||
// 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.
|
||||
|
@ -181,6 +181,7 @@ func newTestChannelDB(clock clock.Clock) (*channeldb.DB, func(), error) {
|
||||
type testContext struct {
|
||||
cdb *channeldb.DB
|
||||
registry *InvoiceRegistry
|
||||
notifier *mockChainNotifier
|
||||
clock *clock.TestClock
|
||||
|
||||
cleanup func()
|
||||
@ -195,8 +196,10 @@ func newTestContext(t *testing.T) *testContext {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
notifier := newMockNotifier()
|
||||
|
||||
expiryWatcher := NewInvoiceExpiryWatcher(
|
||||
clock, 0, uint32(testCurrentHeight), nil, newMockNotifier(),
|
||||
clock, 0, uint32(testCurrentHeight), nil, notifier,
|
||||
)
|
||||
|
||||
// Instantiate and start the invoice ctx.registry.
|
||||
@ -216,6 +219,7 @@ func newTestContext(t *testing.T) *testContext {
|
||||
ctx := testContext{
|
||||
cdb: cdb,
|
||||
registry: registry,
|
||||
notifier: notifier,
|
||||
clock: clock,
|
||||
t: t,
|
||||
cleanup: func() {
|
||||
|
Loading…
Reference in New Issue
Block a user