Skip to content

Commit 85fea72

Browse files
authored
Broadcast commit tx when nothing at stake (#2360)
When we have nothing at stake (channel was never used and we don't have funds to claim), we previously directly went to the CLOSED state without publishing our commitment. This can be an issue for our peer if they have lost data or had a hard time getting a funding tx confirmed. We now publish our commitment once to help them get their funds back in all cases and avoid the CSV delays when getting their funds back. Fixes #1730
1 parent 8a42246 commit 85fea72

File tree

2 files changed

+19
-4
lines changed

2 files changed

+19
-4
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ trait ErrorHandlers extends CommonHandlers {
8383
context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = true))
8484

8585
d match {
86-
case dd: PersistentChannelData if Closing.nothingAtStake(dd) => goto(CLOSED)
86+
case dd: PersistentChannelData if Closing.nothingAtStake(dd) =>
87+
// The channel was never used and we don't have any funds: we don't need to publish our commitment, but it's a
88+
// nice thing to do because it lets our peer get their funds back without delays.
89+
val commitTx = dd.commitments.fullySignedLocalCommitTx(keyManager)
90+
txPublisher ! PublishFinalTx(commitTx, 0 sat, None)
91+
goto(CLOSED)
8792
case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) =>
8893
log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}")
8994
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
@@ -127,7 +132,13 @@ trait ErrorHandlers extends CommonHandlers {
127132
case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) =>
128133
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
129134
handleMutualClose(bestUnpublishedClosingTx, Left(negotiating))
130-
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent
135+
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) =>
136+
// The channel was never used and the funding tx could be double-spent: we don't need to publish our commitment
137+
// since we don't have funds in the channel, but it's a nice thing to do because it lets our peer get their
138+
// funds back without delays if they can't double-spend the funding tx.
139+
val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager)
140+
txPublisher ! PublishFinalTx(commitTx, 0 sat, None)
141+
goto(CLOSED)
131142
case hasCommitments: PersistentChannelData => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that)
132143
case _: TransientChannelData => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs
133144
}

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import fr.acinq.eclair.channel._
2424
import fr.acinq.eclair.channel.fsm.Channel
2525
import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT}
2626
import fr.acinq.eclair.channel.publish.TxPublisher
27+
import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx
2728
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
2829
import fr.acinq.eclair.transactions.Scripts.multiSig2of2
2930
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream}
@@ -268,8 +269,11 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
268269

269270
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f =>
270271
import f._
271-
bob ! Error(ByteVector32.Zeroes, "funding double-spent")
272-
bob2blockchain.expectNoMessage(100 millis) // we don't publish our commit tx when we have nothing at stake
272+
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
273+
bob ! Error(ByteVector32.Zeroes, "please help me recover my funds")
274+
// We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly.
275+
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid)
276+
bob2blockchain.expectNoMessage(100 millis)
273277
awaitCond(bob.stateName == CLOSED)
274278
}
275279

0 commit comments

Comments
 (0)