diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 60f31e3e0..9e735dcff 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -566,4 +566,8 @@ var allTestCases = []*lntest.TestCase{ Name: "fail funding flow psbt", TestFunc: testPsbtChanFundingFailFlow, }, + { + Name: "coop close with htlcs", + TestFunc: testCoopCloseWithHtlcs, + }, } diff --git a/itest/lnd_coop_close_with_htlcs_test.go b/itest/lnd_coop_close_with_htlcs_test.go new file mode 100644 index 000000000..3978e1192 --- /dev/null +++ b/itest/lnd_coop_close_with_htlcs_test.go @@ -0,0 +1,103 @@ +package itest + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +// testCoopCloseWithHtlcs tests whether or not we can successfully issue a coop +// close request whilt there are still active htlcs on the link. Here we will +// set up an HODL invoice to suspend settlement. Then we will attempt to close +// the channel which should appear as a noop for the time being. Then we will +// have the receiver settle the invoice and observe that the channel gets torn +// down after settlement. +func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) { + alice, bob := ht.Alice, ht.Bob + + // Here we set up a channel between Alice and Bob, beginning with a + // balance on Bob's side. + chanPoint := ht.OpenChannel(bob, alice, lntest.OpenChannelParams{ + Amt: btcutil.Amount(1000000), + }) + + // Wait for Bob to understand that the channel is ready to use. + ht.AssertTopologyChannelOpen(bob, chanPoint) + + // Here we set things up so that Alice generates a HODL invoice so we + // can test whether the shutdown is deferred until the settlement of + // that invoice. + payAmt := btcutil.Amount(4) + var preimage lntypes.Preimage + copy(preimage[:], ht.Random32Bytes()) + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Memo: "testing close", + Value: int64(payAmt), + Hash: payHash[:], + } + resp := alice.RPC.AddHoldInvoice(invoiceReq) + invoiceStream := alice.RPC.SubscribeSingleInvoice(payHash[:]) + + // Here we wait for the invoice to be open and payable. + ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_OPEN) + + // Now that the invoice is ready to be paid, let's have Bob open an + // HTLC for it. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: resp.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitSat: 1000000, + } + ht.SendPaymentAndAssertStatus(bob, req, lnrpc.Payment_IN_FLIGHT) + ht.AssertNumActiveHtlcs(bob, 1) + + // Assert at this point that the HTLC is open but not yet settled. + ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_ACCEPTED) + + // Have alice attempt to close the channel. + closeClient := alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{ + ChannelPoint: chanPoint, + NoWait: true, + }) + ht.AssertChannelInactive(bob, chanPoint) + + // Now that the channel is inactive we can be certain that the deferred + // closure is set up. Let's settle the invoice. + alice.RPC.SettleInvoice(preimage[:]) + + // Pull the instant update off the wire to clear the path for the + // close pending update. + _, err := closeClient.Recv() + require.NoError(ht, err) + + // Wait for the next channel closure update. Now that we have settled + // the only HTLC this should be imminent. + update, err := closeClient.Recv() + require.NoError(ht, err) + + // This next update should be a GetClosePending as it should be the + // negotiation of the coop close tx. + closePending := update.GetClosePending() + require.NotNil(ht, closePending) + + // Convert the txid we get from the PendingUpdate to a Hash so we can + // wait for it to be mined. + var closeTxid chainhash.Hash + require.NoError( + ht, closeTxid.SetBytes(closePending.Txid), + "invalid closing txid", + ) + + // Wait for the close tx to be in the Mempool. + ht.Miner.AssertTxInMempool(&closeTxid) + + // Wait for it to get mined and finish tearing down. + ht.AssertStreamChannelCoopClosed(alice, chanPoint, false, closeClient) +}