Catch close commands in `Offline(WaitForDualFundingSigned)` (#3159)
What changed, and why it matters
This patch fixes a state-handling bug in Eclair, a Bitcoin Lightning Network node implementation. When a new dual-funded channel was stuck waiting for both parties to sign, and the node went offline, a force-close command was silently ignored. The fix makes the node properly abort the channel and roll back the funding transaction attempt, preventing funds from being left in limbo.
Reviewers should confirm that `rollbackFundingAttempt` is safe to call from the Offline state and that no race exists with a concurrent reconnection or signature arrival. Operators should update to include this patch if running dual-funding channels.
Security signals we found
State machine missing handler for force-close command in offline dual-funding state
Funds could be left in a non-closable, non-finalized channel state
Patch adds rollback of unconfirmed funding transaction before fast close
Test coverage added for offline force-close during WaitForDualFundingSigned
Evidence from the diff
In Channel.scala, the Event(c: CMD_FORCECLOSE, d) handler now matches DATA_WAIT_FOR_DUAL_FUNDING_SIGNED while in the Offline state. It calls rollbackFundingAttempt on the in-flight funding transaction and then handleFastClose. Previously, this data type was not matched, so the force-close command fell through and was effectively ignored because no commitment existed yet. A test is added to verify both Alice and Bob can force-close while offline in this state, that the funding attempt is rolled back, and that the channel transitions to CLOSED.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scalaWaitForDualFundingSigned / Offline channel stateCMD_FORCECLOSE command handlingInspect captured patch +28 / −0
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
index 7a4e1dd..86ddaa3 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
@@ -2866,6 +2866,9 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(c: CMD_FORCECLOSE, d) =>
d match {
+ case data: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED =>
+ rollbackFundingAttempt(data.signingSession.fundingTx.tx, Nil)
+ handleFastClose(c, d.channelId)
case data: ChannelDataWithCommitments =>
val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo
val failure = ForcedLocalCommit(d.channelId)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala
index 2137dab..d0a2fc9 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala
@@ -364,6 +364,31 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
awaitCond(bob.stateName == CLOSED)
}
+ test("recv CMD_FORCECLOSE (offline)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
+ import f._
+
+ alice ! INPUT_DISCONNECTED
+ awaitCond(alice.stateName == OFFLINE)
+ bob ! INPUT_DISCONNECTED
+ awaitCond(bob.stateName == OFFLINE)
+
+ val finalChannelId = channelId(alice)
+ val sender = TestProbe()
+ val c = CMD_FORCECLOSE(sender.ref)
+
+ alice ! c
+ sender.expectMsg(RES_SUCCESS(c, finalChannelId))
+ awaitCond(wallet.rolledback.size == 1)
+ aliceListener.expectMsgType[ChannelAborted]
+ awaitCond(alice.stateName == CLOSED)
+
+ bob ! c
+ sender.expectMsg(RES_SUCCESS(c, finalChannelId))
+ awaitCond(wallet.rolledback.size == 2)
+ bobListener.expectMsgType[ChannelAborted]
+ awaitCond(bob.stateName == CLOSED)
+ }
+
test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._
Why this scored 43/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.