invoices/test: add height expiry watcher tests with registry
This commit is contained in:
parent
8066ff7047
commit
74373f26b9
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user