|
1 | 1 | package itest |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "testing" |
| 5 | + |
4 | 6 | "github.com/btcsuite/btcd/btcutil" |
5 | 7 | "github.com/btcsuite/btcd/chaincfg/chainhash" |
6 | 8 | "github.com/lightningnetwork/lnd/lnrpc" |
7 | 9 | "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" |
8 | 10 | "github.com/lightningnetwork/lnd/lnrpc/routerrpc" |
| 11 | + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" |
9 | 12 | "github.com/lightningnetwork/lnd/lntest" |
| 13 | + "github.com/lightningnetwork/lnd/lntest/wait" |
10 | 14 | "github.com/lightningnetwork/lnd/lntypes" |
11 | 15 | "github.com/stretchr/testify/require" |
12 | 16 | ) |
13 | 17 |
|
14 | 18 | // testCoopCloseWithHtlcs tests whether we can successfully issue a coop close |
15 | | -// request while there are still active htlcs on the link. Here we will set up |
16 | | -// an HODL invoice to suspend settlement. Then we will attempt to close the |
17 | | -// channel which should appear as a noop for the time being. Then we will have |
18 | | -// the receiver settle the invoice and observe that the channel gets torn down |
19 | | -// after settlement. |
| 19 | +// request while there are still active htlcs on the link. In all the tests, we |
| 20 | +// will set up an HODL invoice to suspend settlement. Then we will attempt to |
| 21 | +// close the channel which should appear as a noop for the time being. Then we |
| 22 | +// will have the receiver settle the invoice and observe that the channel gets |
| 23 | +// torn down after settlement. |
20 | 24 | func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) { |
| 25 | + ht.Run("no restart", func(t *testing.T) { |
| 26 | + tt := ht.Subtest(t) |
| 27 | + coopCloseWithHTLCs(tt) |
| 28 | + }) |
| 29 | + |
| 30 | + ht.Run("with restart", func(t *testing.T) { |
| 31 | + tt := ht.Subtest(t) |
| 32 | + coopCloseWithHTLCsWithRestart(tt) |
| 33 | + }) |
| 34 | +} |
| 35 | + |
| 36 | +// coopCloseWithHTLCs tests the basic coop close scenario which occurs when one |
| 37 | +// channel party initiates a channel shutdown while an HTLC is still pending on |
| 38 | +// the channel. |
| 39 | +func coopCloseWithHTLCs(ht *lntest.HarnessTest) { |
21 | 40 | alice, bob := ht.Alice, ht.Bob |
| 41 | + ht.ConnectNodes(alice, bob) |
22 | 42 |
|
23 | 43 | // Here we set up a channel between Alice and Bob, beginning with a |
24 | 44 | // balance on Bob's side. |
@@ -101,3 +121,130 @@ func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) { |
101 | 121 | // Wait for it to get mined and finish tearing down. |
102 | 122 | ht.AssertStreamChannelCoopClosed(alice, chanPoint, false, closeClient) |
103 | 123 | } |
| 124 | + |
| 125 | +// coopCloseWithHTLCsWithRestart also tests the coop close flow when an HTLC |
| 126 | +// is still pending on the channel but this time it ensures that the shutdown |
| 127 | +// process continues as expected even if a channel re-establish happens after |
| 128 | +// one party has already initiated the shutdown. |
| 129 | +func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) { |
| 130 | + alice, bob := ht.Alice, ht.Bob |
| 131 | + ht.ConnectNodes(alice, bob) |
| 132 | + |
| 133 | + // Open a channel between Alice and Bob with the balance split equally. |
| 134 | + // We do this to ensure that the close transaction will have 2 outputs |
| 135 | + // so that we can assert that the correct delivery address gets used by |
| 136 | + // the channel close initiator. |
| 137 | + chanPoint := ht.OpenChannel(bob, alice, lntest.OpenChannelParams{ |
| 138 | + Amt: btcutil.Amount(1000000), |
| 139 | + PushAmt: btcutil.Amount(1000000 / 2), |
| 140 | + }) |
| 141 | + |
| 142 | + // Wait for Bob to understand that the channel is ready to use. |
| 143 | + ht.AssertTopologyChannelOpen(bob, chanPoint) |
| 144 | + |
| 145 | + // Set up a HODL invoice so that we can be sure that an HTLC is pending |
| 146 | + // on the channel at the time that shutdown is requested. |
| 147 | + var preimage lntypes.Preimage |
| 148 | + copy(preimage[:], ht.Random32Bytes()) |
| 149 | + payHash := preimage.Hash() |
| 150 | + |
| 151 | + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ |
| 152 | + Memo: "testing close", |
| 153 | + Value: 400, |
| 154 | + Hash: payHash[:], |
| 155 | + } |
| 156 | + resp := alice.RPC.AddHoldInvoice(invoiceReq) |
| 157 | + invoiceStream := alice.RPC.SubscribeSingleInvoice(payHash[:]) |
| 158 | + |
| 159 | + // Wait for the invoice to be ready and payable. |
| 160 | + ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_OPEN) |
| 161 | + |
| 162 | + // Now that the invoice is ready to be paid, let's have Bob open an HTLC |
| 163 | + // for it. |
| 164 | + req := &routerrpc.SendPaymentRequest{ |
| 165 | + PaymentRequest: resp.PaymentRequest, |
| 166 | + TimeoutSeconds: 60, |
| 167 | + FeeLimitSat: 1000000, |
| 168 | + } |
| 169 | + ht.SendPaymentAndAssertStatus(bob, req, lnrpc.Payment_IN_FLIGHT) |
| 170 | + ht.AssertNumActiveHtlcs(bob, 1) |
| 171 | + |
| 172 | + // Assert at this point that the HTLC is open but not yet settled. |
| 173 | + ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_ACCEPTED) |
| 174 | + |
| 175 | + // We will now let Alice initiate the closure of the channel. We will |
| 176 | + // also let her specify a specific delivery address to be used since we |
| 177 | + // want to test that this same address is used in the Shutdown message |
| 178 | + // on reconnection. |
| 179 | + newAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ |
| 180 | + Type: AddrTypeWitnessPubkeyHash, |
| 181 | + }) |
| 182 | + |
| 183 | + _ = alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{ |
| 184 | + ChannelPoint: chanPoint, |
| 185 | + NoWait: true, |
| 186 | + DeliveryAddress: newAddr.Address, |
| 187 | + }) |
| 188 | + |
| 189 | + // Assert that both nodes now see this channel as inactive. |
| 190 | + ht.AssertChannelInactive(bob, chanPoint) |
| 191 | + ht.AssertChannelInactive(alice, chanPoint) |
| 192 | + |
| 193 | + // Now restart Alice and Bob. |
| 194 | + ht.RestartNode(alice) |
| 195 | + ht.RestartNode(bob) |
| 196 | + |
| 197 | + ht.AssertConnected(alice, bob) |
| 198 | + |
| 199 | + // Show that the channel is seen as active again by Alice and Bob. |
| 200 | + // |
| 201 | + // NOTE: This is a bug and will be fixed in an upcoming commit. |
| 202 | + ht.AssertChannelActive(alice, chanPoint) |
| 203 | + ht.AssertChannelActive(bob, chanPoint) |
| 204 | + |
| 205 | + // Let's settle the invoice. |
| 206 | + alice.RPC.SettleInvoice(preimage[:]) |
| 207 | + |
| 208 | + // Wait for the channel to appear in the waiting closed list. |
| 209 | + // |
| 210 | + // NOTE: this will time out at the moment since there is a bug that |
| 211 | + // results in shutdown not properly being re-started after a reconnect. |
| 212 | + err := wait.Predicate(func() bool { |
| 213 | + pendingChansResp := alice.RPC.PendingChannels() |
| 214 | + waitingClosed := pendingChansResp.WaitingCloseChannels |
| 215 | + |
| 216 | + return len(waitingClosed) == 1 |
| 217 | + }, defaultTimeout) |
| 218 | + |
| 219 | + // We assert here that there is a timeout error. This will be fixed in |
| 220 | + // an upcoming commit. |
| 221 | + require.Error(ht, err) |
| 222 | + |
| 223 | + // Since the channel closure did not continue, we need to re-init the |
| 224 | + // close. |
| 225 | + closingTXID := ht.CloseChannel(alice, chanPoint) |
| 226 | + |
| 227 | + // To further demonstrate the extent of the bug, we inspect the closing |
| 228 | + // transaction here to show that the delivery address that Alice |
| 229 | + // specified in her original close request is not the one that ended up |
| 230 | + // being used. |
| 231 | + // |
| 232 | + // NOTE: this is a bug that will be fixed in an upcoming commit. |
| 233 | + tx := alice.RPC.GetTransaction(&walletrpc.GetTransactionRequest{ |
| 234 | + Txid: closingTXID.String(), |
| 235 | + }) |
| 236 | + require.Len(ht, tx.OutputDetails, 2) |
| 237 | + |
| 238 | + // Find Alice's output in the coop-close transaction. |
| 239 | + var outputDetail *lnrpc.OutputDetail |
| 240 | + for _, output := range tx.OutputDetails { |
| 241 | + if output.IsOurAddress { |
| 242 | + outputDetail = output |
| 243 | + break |
| 244 | + } |
| 245 | + } |
| 246 | + require.NotNil(ht, outputDetail) |
| 247 | + |
| 248 | + // Show that the address used is not the one she requested. |
| 249 | + require.NotEqual(ht, outputDetail.Address, newAddr.Address) |
| 250 | +} |
0 commit comments