|
|
|
@ -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.
|
|
|
|
|