Skip to content

Commit 987604e

Browse files
committed
itest: demonstrate shutdown on restart bug
This commit adds an itest to demonstrate that the following bug exists: If channel Shutdown is initiated but then a re-connect is done before the shutdown is complete, then the initiating node currently does not properly resend the Shutdown message as required by the spec. This will be fixed in an upcoming commit.
1 parent 8c064b8 commit 987604e

File tree

1 file changed

+152
-5
lines changed

1 file changed

+152
-5
lines changed

itest/lnd_coop_close_with_htlcs_test.go

Lines changed: 152 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,44 @@
11
package itest
22

33
import (
4+
"testing"
5+
46
"github.com/btcsuite/btcd/btcutil"
57
"github.com/btcsuite/btcd/chaincfg/chainhash"
68
"github.com/lightningnetwork/lnd/lnrpc"
79
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
810
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
11+
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
912
"github.com/lightningnetwork/lnd/lntest"
13+
"github.com/lightningnetwork/lnd/lntest/wait"
1014
"github.com/lightningnetwork/lnd/lntypes"
1115
"github.com/stretchr/testify/require"
1216
)
1317

1418
// 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.
2024
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) {
2140
alice, bob := ht.Alice, ht.Bob
41+
ht.ConnectNodes(alice, bob)
2242

2343
// Here we set up a channel between Alice and Bob, beginning with a
2444
// balance on Bob's side.
@@ -101,3 +121,130 @@ func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) {
101121
// Wait for it to get mined and finish tearing down.
102122
ht.AssertStreamChannelCoopClosed(alice, chanPoint, false, closeClient)
103123
}
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

Comments
 (0)