1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 22:46:44 +01:00

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
This commit is contained in:
Bastien Teinturier 2022-08-03 18:03:20 +02:00 committed by GitHub
parent 8a42246b18
commit 85fea72720
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 19 additions and 4 deletions

View file

@ -83,7 +83,12 @@ trait ErrorHandlers extends CommonHandlers {
context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = true))
d match {
case dd: PersistentChannelData if Closing.nothingAtStake(dd) => goto(CLOSED)
case dd: PersistentChannelData if Closing.nothingAtStake(dd) =>
// The channel was never used and we don't have any funds: we don't need to publish our commitment, but it's a
// nice thing to do because it lets our peer get their funds back without delays.
val commitTx = dd.commitments.fullySignedLocalCommitTx(keyManager)
txPublisher ! PublishFinalTx(commitTx, 0 sat, None)
goto(CLOSED)
case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) =>
log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}")
// 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 {
case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) =>
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
handleMutualClose(bestUnpublishedClosingTx, Left(negotiating))
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
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) =>
// The channel was never used and the funding tx could be double-spent: we don't need to publish our commitment
// since we don't have funds in the channel, but it's a nice thing to do because it lets our peer get their
// funds back without delays if they can't double-spend the funding tx.
val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager)
txPublisher ! PublishFinalTx(commitTx, 0 sat, None)
goto(CLOSED)
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)
case _: TransientChannelData => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs
}

View file

@ -24,6 +24,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT}
import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.transactions.Scripts.multiSig2of2
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream}
@ -268,8 +269,11 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f =>
import f._
bob ! Error(ByteVector32.Zeroes, "funding double-spent")
bob2blockchain.expectNoMessage(100 millis) // we don't publish our commit tx when we have nothing at stake
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! Error(ByteVector32.Zeroes, "please help me recover my funds")
// We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly.
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid)
bob2blockchain.expectNoMessage(100 millis)
awaitCond(bob.stateName == CLOSED)
}