Skip to content

Commit 1227eb1

Browse files
authored
Merge pull request #9491 from ziggie1984/closechannel-rpc
Allow coop closing a channel with HTLCs on it via lncli
2 parents 27440e8 + 8017139 commit 1227eb1

File tree

13 files changed

+2465
-2279
lines changed

13 files changed

+2465
-2279
lines changed

cmd/commands/commands.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,11 @@ var closeChannelCommand = cli.Command{
10111011
comparison is the end boundary of the fee negotiation, if not specified
10121012
it's always x3 of the starting value. Increasing this value increases
10131013
the chance of a successful negotiation.
1014+
Moreover if the channel has active HTLCs on it, the coop close will
1015+
wait until all HTLCs are resolved and will not allow any new HTLCs on
1016+
the channel. The channel will appear as disabled in the listchannels
1017+
output. The command will block in that case until the channel close tx
1018+
is broadcasted.
10141019
10151020
In the case of a cooperative closure, one can manually set the address
10161021
to deliver funds to upon closure. This is optional, and may only be used
@@ -1042,8 +1047,10 @@ var closeChannelCommand = cli.Command{
10421047
Usage: "attempt an uncooperative closure",
10431048
},
10441049
cli.BoolFlag{
1045-
Name: "block",
1046-
Usage: "block until the channel is closed",
1050+
Name: "block",
1051+
Usage: `block will wait for the channel to be closed,
1052+
"meaning that it will wait for the channel close tx to
1053+
get 1 confirmation.`,
10471054
},
10481055
cli.Int64Flag{
10491056
Name: "conf_target",
@@ -1117,6 +1124,9 @@ func closeChannel(ctx *cli.Context) error {
11171124
SatPerVbyte: ctx.Uint64(feeRateFlag),
11181125
DeliveryAddress: ctx.String("delivery_addr"),
11191126
MaxFeePerVbyte: ctx.Uint64("max_fee_rate"),
1127+
// This makes sure that a coop close will also be executed if
1128+
// active HTLCs are present on the channel.
1129+
NoWait: true,
11201130
}
11211131

11221132
// After parsing the request, we'll spin up a goroutine that will
@@ -1154,7 +1164,9 @@ func closeChannel(ctx *cli.Context) error {
11541164
// executeChannelClose attempts to close the channel from a request. The closing
11551165
// transaction ID is sent through `txidChan` as soon as it is broadcasted to the
11561166
// network. The block boolean is used to determine if we should block until the
1157-
// closing transaction receives all of its required confirmations.
1167+
// closing transaction receives a confirmation of 1 block. The logging outputs
1168+
// are sent to stderr to avoid conflicts with the JSON output of the command
1169+
// and potential work flows which depend on a proper JSON output.
11581170
func executeChannelClose(ctxc context.Context, client lnrpc.LightningClient,
11591171
req *lnrpc.CloseChannelRequest, txidChan chan<- string, block bool) error {
11601172

@@ -1173,22 +1185,40 @@ func executeChannelClose(ctxc context.Context, client lnrpc.LightningClient,
11731185

11741186
switch update := resp.Update.(type) {
11751187
case *lnrpc.CloseStatusUpdate_CloseInstant:
1176-
if req.NoWait {
1177-
return nil
1188+
fmt.Fprintln(os.Stderr, "Channel close successfully "+
1189+
"initiated")
1190+
1191+
pendingHtlcs := update.CloseInstant.NumPendingHtlcs
1192+
if pendingHtlcs > 0 {
1193+
fmt.Fprintf(os.Stderr, "Cooperative channel "+
1194+
"close waiting for %d HTLCs to be "+
1195+
"resolved before the close process "+
1196+
"can kick off\n", pendingHtlcs)
11781197
}
1198+
11791199
case *lnrpc.CloseStatusUpdate_ClosePending:
11801200
closingHash := update.ClosePending.Txid
11811201
txid, err := chainhash.NewHash(closingHash)
11821202
if err != nil {
11831203
return err
11841204
}
11851205

1206+
fmt.Fprintf(os.Stderr, "Channel close transaction "+
1207+
"broadcasted: %v\n", txid)
1208+
11861209
txidChan <- txid.String()
11871210

11881211
if !block {
11891212
return nil
11901213
}
1214+
1215+
fmt.Fprintln(os.Stderr, "Waiting for channel close "+
1216+
"confirmation ...")
1217+
11911218
case *lnrpc.CloseStatusUpdate_ChanClose:
1219+
fmt.Fprintln(os.Stderr, "Channel close successfully "+
1220+
"confirmed")
1221+
11921222
return nil
11931223
}
11941224
}

docs/release-notes/release-notes-0.19.0.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@
9393
config is added `disable-backup-archive`, with default set to false, to
9494
determine if previous channel backups should be archived or not.
9595

96+
* [The max fee rate](https://github.com/lightningnetwork/lnd/pull/9491) is now
97+
respected when a coop close is initiated. Before the max fee rate would only
98+
be effective for the remote party in the negotiation.
99+
96100
## Functional Enhancements
97101
* [Add ability](https://github.com/lightningnetwork/lnd/pull/8998) to paginate
98102
wallet transactions.
@@ -137,6 +141,11 @@
137141
the misnomer of `chan_id` which was describing the short channel
138142
id to `scid` to represent what it really is.
139143

144+
* [In the coop close](https://github.com/lightningnetwork/lnd/pull/9491) case
145+
we always initiate the cooperative close flow even if there are HTLCs active
146+
on the channel. LND will disable the channel for new HTLCs and kick off the
147+
cooperative close flow automatically when the channel has no HTLCs left.
148+
140149
# Improvements
141150
## Functional Updates
142151

htlcswitch/switch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1556,7 +1556,7 @@ out:
15561556
s.indexMtx.RUnlock()
15571557

15581558
peerPub := link.PeerPubKey()
1559-
log.Debugf("Requesting local channel close: peer=%v, "+
1559+
log.Debugf("Requesting local channel close: peer=%x, "+
15601560
"chan_id=%x", link.PeerPubKey(), chanID[:])
15611561

15621562
go s.cfg.LocalChannelClose(peerPub[:], req)

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,10 @@ var allTestCases = []*lntest.TestCase{
602602
Name: "coop close with htlcs",
603603
TestFunc: testCoopCloseWithHtlcs,
604604
},
605+
{
606+
Name: "coop close exceeds max fee",
607+
TestFunc: testCoopCloseExceedsMaxFee,
608+
},
605609
{
606610
Name: "open channel locked balance",
607611
TestFunc: testOpenChannelLockedBalance,

itest/lnd_channel_backup_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,16 @@ func (c *chanRestoreScenario) testScenario(ht *lntest.HarnessTest,
283283

284284
// We don't get an error directly but only when reading the first
285285
// message of the stream.
286-
err := ht.CloseChannelAssertErr(
287-
dave, &lnrpc.ChannelPoint{
286+
req := &lnrpc.CloseChannelRequest{
287+
ChannelPoint: &lnrpc.ChannelPoint{
288288
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
289289
FundingTxidStr: chanPointParts[0],
290290
},
291291
OutputIndex: uint32(chanPointIndex),
292-
}, true,
293-
)
292+
},
293+
Force: true,
294+
}
295+
err := ht.CloseChannelAssertErr(dave, req)
294296
require.Contains(ht, err.Error(), "cannot close channel with state: ")
295297
require.Contains(ht, err.Error(), "ChanStatusRestored")
296298

itest/lnd_coop_close_with_htlcs_test.go

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/lightningnetwork/lnd/lntest"
1313
"github.com/lightningnetwork/lnd/lntest/wait"
1414
"github.com/lightningnetwork/lnd/lntypes"
15+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1516
"github.com/stretchr/testify/require"
1617
)
1718

@@ -93,14 +94,17 @@ func coopCloseWithHTLCs(ht *lntest.HarnessTest) {
9394
// closure is set up. Let's settle the invoice.
9495
alice.RPC.SettleInvoice(preimage[:])
9596

96-
// Pull the instant update off the wire to clear the path for the
97-
// close pending update.
98-
_, err := closeClient.Recv()
97+
// Pull the instant update off the wire and make sure the number of
98+
// pending HTLCs is as expected.
99+
update, err := closeClient.Recv()
99100
require.NoError(ht, err)
101+
closeInstant := update.GetCloseInstant()
102+
require.NotNil(ht, closeInstant)
103+
require.Equal(ht, closeInstant.NumPendingHtlcs, int32(1))
100104

101105
// Wait for the next channel closure update. Now that we have settled
102106
// the only HTLC this should be imminent.
103-
update, err := closeClient.Recv()
107+
update, err = closeClient.Recv()
104108
require.NoError(ht, err)
105109

106110
// This next update should be a GetClosePending as it should be the
@@ -243,3 +247,76 @@ func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) {
243247
// Show that the address used is the one she requested.
244248
require.Equal(ht, outputDetail.Address, newAddr.Address)
245249
}
250+
251+
// testCoopCloseExceedsMaxFee tests that we fail the coop close process if
252+
// the max fee rate exceeds the expected fee rate for the initial closing fee
253+
// proposal.
254+
func testCoopCloseExceedsMaxFee(ht *lntest.HarnessTest) {
255+
const chanAmt = 1000000
256+
257+
// Create a channel Alice->Bob.
258+
chanPoints, nodes := ht.CreateSimpleNetwork(
259+
[][]string{nil, nil}, lntest.OpenChannelParams{
260+
Amt: chanAmt,
261+
},
262+
)
263+
264+
alice, _ := nodes[0], nodes[1]
265+
chanPoint := chanPoints[0]
266+
267+
// Set the fee estimate for one block to 10 sat/vbyte.
268+
ht.SetFeeEstimateWithConf(chainfee.SatPerVByte(10).FeePerKWeight(), 1)
269+
270+
// Have alice attempt to close the channel. We expect the initial fee
271+
// rate to exceed the max fee rate for the closing transaction so we
272+
// fail the closing process.
273+
req := &lnrpc.CloseChannelRequest{
274+
ChannelPoint: chanPoint,
275+
MaxFeePerVbyte: 5,
276+
NoWait: true,
277+
TargetConf: 1,
278+
}
279+
err := ht.CloseChannelAssertErr(alice, req)
280+
require.Contains(ht, err.Error(), "max_fee_per_vbyte (1250 sat/kw) is "+
281+
"less than the required fee rate (2500 sat/kw)")
282+
283+
// Now close the channel with a appropriate max fee rate.
284+
closeClient := alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{
285+
ChannelPoint: chanPoint,
286+
NoWait: true,
287+
TargetConf: 1,
288+
MaxFeePerVbyte: 10,
289+
})
290+
291+
// Pull the instant update off the wire to clear the path for the
292+
// close pending update. Moreover confirm that there are no pending
293+
// HTLCs on the channel.
294+
update, err := closeClient.Recv()
295+
require.NoError(ht, err)
296+
closeInstant := update.GetCloseInstant()
297+
require.NotNil(ht, closeInstant)
298+
require.Equal(ht, closeInstant.NumPendingHtlcs, int32(0))
299+
300+
// Wait for the channel to be closed.
301+
update, err = closeClient.Recv()
302+
require.NoError(ht, err)
303+
304+
// This next update should be a GetClosePending as it should be the
305+
// negotiation of the coop close tx.
306+
closePending := update.GetClosePending()
307+
require.NotNil(ht, closePending)
308+
309+
// Convert the txid we get from the PendingUpdate to a Hash so we can
310+
// wait for it to be mined.
311+
var closeTxid chainhash.Hash
312+
require.NoError(
313+
ht, closeTxid.SetBytes(closePending.Txid),
314+
"invalid closing txid",
315+
)
316+
317+
// Wait for the close tx to be in the Mempool.
318+
ht.AssertTxInMempool(closeTxid)
319+
320+
// Wait for it to get mined and finish tearing down.
321+
ht.AssertStreamChannelCoopClosed(alice, chanPoint, false, closeClient)
322+
}

itest/lnd_funding_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,10 @@ func runExternalFundingScriptEnforced(ht *lntest.HarnessTest) {
674674
// First, we'll try to close the channel as Carol, the initiator. This
675675
// should fail as a frozen channel only allows the responder to
676676
// initiate a channel close.
677-
err := ht.CloseChannelAssertErr(carol, chanPoint2, false)
677+
req := &lnrpc.CloseChannelRequest{
678+
ChannelPoint: chanPoint2,
679+
}
680+
err := ht.CloseChannelAssertErr(carol, req)
678681
require.Contains(ht, err.Error(), "cannot co-op close frozen channel")
679682

680683
// Before Dave closes the channel, he needs to check the invoice is
@@ -831,7 +834,10 @@ func runExternalFundingTaproot(ht *lntest.HarnessTest) {
831834
// First, we'll try to close the channel as Carol, the initiator. This
832835
// should fail as a frozen channel only allows the responder to
833836
// initiate a channel close.
834-
err := ht.CloseChannelAssertErr(carol, chanPoint2, false)
837+
req := &lnrpc.CloseChannelRequest{
838+
ChannelPoint: chanPoint2,
839+
}
840+
err := ht.CloseChannelAssertErr(carol, req)
835841
require.Contains(ht, err.Error(), "cannot co-op close frozen channel")
836842

837843
// Before Dave closes the channel, he needs to check the invoice is

0 commit comments

Comments
 (0)