lnd/itest/lnd_coop_close_with_htlcs_test.go

246 lines
8.2 KiB
Go

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)
}