package itest import ( "testing" "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/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) // testCoopCloseWithHtlcs tests whether we can successfully issue a coop close // request while there are still active htlcs on the link. In all the tests, 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) { ht.Run("no restart", func(t *testing.T) { tt := ht.Subtest(t) coopCloseWithHTLCs(tt) }) ht.Run("with restart", func(t *testing.T) { tt := ht.Subtest(t) coopCloseWithHTLCsWithRestart(tt) }) } // coopCloseWithHTLCs tests the basic coop close scenario which occurs when one // channel party initiates a channel shutdown while an HTLC is still pending on // the channel. func coopCloseWithHTLCs(ht *lntest.HarnessTest) { alice, bob := ht.Alice, ht.Bob ht.ConnectNodes(alice, 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, TargetConf: 6, }) 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) } // coopCloseWithHTLCsWithRestart also tests the coop close flow when an HTLC // is still pending on the channel but this time it ensures that the shutdown // process continues as expected even if a channel re-establish happens after // one party has already initiated the shutdown. func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) { alice, bob := ht.Alice, ht.Bob ht.ConnectNodes(alice, bob) // Open a channel between Alice and Bob with the balance split equally. // We do this to ensure that the close transaction will have 2 outputs // so that we can assert that the correct delivery address gets used by // the channel close initiator. chanPoint := ht.OpenChannel(bob, alice, lntest.OpenChannelParams{ Amt: btcutil.Amount(1000000), PushAmt: btcutil.Amount(1000000 / 2), }) // Wait for Bob to understand that the channel is ready to use. ht.AssertTopologyChannelOpen(bob, chanPoint) // Set up a HODL invoice so that we can be sure that an HTLC is pending // on the channel at the time that shutdown is requested. var preimage lntypes.Preimage copy(preimage[:], ht.Random32Bytes()) payHash := preimage.Hash() invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ Memo: "testing close", Value: 400, Hash: payHash[:], } resp := alice.RPC.AddHoldInvoice(invoiceReq) invoiceStream := alice.RPC.SubscribeSingleInvoice(payHash[:]) // Wait for the invoice to be ready 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) // We will now let Alice initiate the closure of the channel. We will // also let her specify a specific delivery address to be used since we // want to test that this same address is used in the Shutdown message // on reconnection. newAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: AddrTypeWitnessPubkeyHash, }) _ = alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{ ChannelPoint: chanPoint, NoWait: true, DeliveryAddress: newAddr.Address, TargetConf: 6, }) // Assert that both nodes see the channel as waiting for close. ht.AssertChannelInactive(bob, chanPoint) ht.AssertChannelInactive(alice, chanPoint) // Now restart Alice and Bob. ht.RestartNode(alice) ht.RestartNode(bob) ht.AssertConnected(alice, bob) // Show that both nodes still see the channel as waiting for close after // the restart. ht.AssertChannelInactive(bob, chanPoint) ht.AssertChannelInactive(alice, chanPoint) // Settle the invoice. alice.RPC.SettleInvoice(preimage[:]) // Wait for the channel to appear in the waiting closed list. err := wait.Predicate(func() bool { pendingChansResp := alice.RPC.PendingChannels() waitingClosed := pendingChansResp.WaitingCloseChannels return len(waitingClosed) == 1 }, defaultTimeout) require.NoError(ht, err) // Wait for the close tx to be in the Mempool and then mine 6 blocks // to confirm the close. closingTx := ht.AssertClosingTxInMempool( chanPoint, lnrpc.CommitmentType_LEGACY, ) ht.MineBlocksAndAssertNumTxes(6, 1) // Finally, we inspect the closing transaction here to show that the // delivery address that Alice specified in her original close request // is the one that ended up being used in the final closing transaction. tx := alice.RPC.GetTransaction(&walletrpc.GetTransactionRequest{ Txid: closingTx.TxHash().String(), }) require.Len(ht, tx.OutputDetails, 2) // Find Alice's output in the coop-close transaction. var outputDetail *lnrpc.OutputDetail for _, output := range tx.OutputDetails { if output.IsOurAddress { outputDetail = output break } } require.NotNil(ht, outputDetail) // Show that the address used is the one she requested. require.Equal(ht, outputDetail.Address, newAddr.Address) }