Skip to content

Commit a17a5eb

Browse files
authored
Allow force-closing in Syncing and Offline states (#223)
It makes sense to allow users to force-close channels that may be stuck in syncing or offline. Fixes #216
1 parent 3dcc579 commit a17a5eb

File tree

3 files changed

+42
-1
lines changed

3 files changed

+42
-1
lines changed

src/commonMain/kotlin/fr/acinq/eclair/channel/Channel.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,15 @@ data class Offline(val state: ChannelStateWithCommitments) : ChannelState() {
749749
else -> Pair(Offline(newState as ChannelStateWithCommitments), actions)
750750
}
751751
}
752+
event is ChannelEvent.ExecuteCommand && event.command is CMD_FORCECLOSE -> {
753+
val (newState, actions) = state.process(event)
754+
when (newState) {
755+
// NB: it doesn't make sense to try to send outgoing messages if we're offline.
756+
is Closing -> Pair(newState, actions.filterNot { it is ChannelAction.Message.Send })
757+
is Closed -> Pair(newState, actions.filterNot { it is ChannelAction.Message.Send })
758+
else -> Pair(Offline(newState as ChannelStateWithCommitments), actions)
759+
}
760+
}
752761
else -> unhandled(event)
753762
}
754763
}
@@ -1011,6 +1020,14 @@ data class Syncing(val state: ChannelStateWithCommitments, val waitForTheirReest
10111020
}
10121021
}
10131022
event is ChannelEvent.Disconnected -> Pair(Offline(state), listOf())
1023+
event is ChannelEvent.ExecuteCommand && event.command is CMD_FORCECLOSE -> {
1024+
val (newState, actions) = state.process(event)
1025+
when (newState) {
1026+
is Closing -> Pair(newState, actions)
1027+
is Closed -> Pair(newState, actions)
1028+
else -> Pair(Syncing(newState as ChannelStateWithCommitments, waitForTheirReestablishMessage), actions)
1029+
}
1030+
}
10141031
else -> unhandled(event)
10151032
}
10161033
}

src/commonTest/kotlin/fr/acinq/eclair/channel/states/OfflineTestsCommon.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,18 @@ class OfflineTestsCommon : EclairTestSuite() {
504504
assertEquals(watchSpent, actions4.findWatches<WatchSpent>().map { OutPoint(lcp.commitTx, it.outputIndex.toLong()) }.toSet())
505505
}
506506

507+
@Test
508+
fun `recv CMD_FORCECLOSE`() {
509+
val (alice, _) = TestsHelper.reachNormal()
510+
val (alice1, _) = alice.processEx(ChannelEvent.Disconnected)
511+
assertTrue(alice1 is Offline)
512+
val commitTx = alice1.state.commitments.localCommit.publishableTxs.commitTx.tx
513+
val (alice2, actions2) = alice1.process(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE))
514+
assertTrue(alice2 is Closing)
515+
actions2.hasTx(commitTx)
516+
assertNull(actions2.findOutgoingMessageOpt<Error>()) // we're offline so we shouldn't try to send messages
517+
}
518+
507519
@Test
508520
fun `restore closing channel`() {
509521
val bob = run {

src/commonTest/kotlin/fr/acinq/eclair/channel/states/SyncingTestsCommon.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package fr.acinq.eclair.channel.states
22

3-
import fr.acinq.bitcoin.Block
43
import fr.acinq.bitcoin.ByteVector
54
import fr.acinq.bitcoin.ScriptFlags
65
import fr.acinq.bitcoin.Transaction
@@ -14,9 +13,11 @@ import fr.acinq.eclair.tests.TestConstants
1413
import fr.acinq.eclair.tests.utils.EclairTestSuite
1514
import fr.acinq.eclair.utils.msat
1615
import fr.acinq.eclair.wire.ChannelReestablish
16+
import fr.acinq.eclair.wire.Error
1717
import fr.acinq.eclair.wire.Init
1818
import kotlin.test.Test
1919
import kotlin.test.assertEquals
20+
import kotlin.test.assertNull
2021
import kotlin.test.assertTrue
2122

2223
class SyncingTestsCommon : EclairTestSuite() {
@@ -58,6 +59,17 @@ class SyncingTestsCommon : EclairTestSuite() {
5859
assertEquals(watches.map { it.txId }.toSet(), setOf(revokedTx.txid, bob1.revokedCommitPublished.first().claimMainOutputTx!!.txid))
5960
}
6061

62+
@Test
63+
fun `recv CMD_FORCECLOSE`() {
64+
val (alice, _, _) = run {
65+
val (alice, bob) = reachNormal()
66+
disconnect(alice, bob)
67+
}
68+
val (alice1, actions1) = alice.process(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE))
69+
assertTrue(alice1 is Closing)
70+
actions1.hasOutgoingMessage<Error>()
71+
}
72+
6173
companion object {
6274
fun disconnect(alice: Normal, bob: Normal): Triple<Syncing, Syncing, Pair<ChannelReestablish, ChannelReestablish>> {
6375
val (alice1, _) = alice.processEx(ChannelEvent.Disconnected)

0 commit comments

Comments
 (0)