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 da9611ca80..bef8a0c3aa 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 @@ -30,7 +30,6 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.channel.ChannelTypes.SimpleTaprootChannelsPhoenix import fr.acinq.eclair.channel.Commitments.PostRevocationAction import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult @@ -2720,10 +2719,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - d.localShutdown.foreach { - localShutdown => - log.debug("re-sending local_shutdown") - sendQueue = sendQueue :+ localShutdown + val shutdown_opt = d.localShutdown match { + case None => None + case Some(shutdown) => + log.debug("re-sending local shutdown") + val shutdown1 = createShutdown(commitments1, shutdown.scriptPubKey) + sendQueue = sendQueue :+ shutdown1 + Some(shutdown1) } if (d.commitments.announceChannel) { @@ -2754,7 +2756,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex) } - goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue + goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1, localShutdown = shutdown_opt) sending sendQueue } } @@ -2780,9 +2782,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall handleSyncFailure(channelReestablish, syncFailure, d) case syncSuccess: SyncResult.Success => val commitments1 = d.commitments.discardUnsignedUpdates() - val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ d.localShutdown + // We retransmit our shutdown: we may have updated our script and they may not have received it. + val shutdown = createShutdown(commitments1, d.localShutdown.scriptPubKey) + val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ shutdown // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - goto(SHUTDOWN) using d.copy(commitments = commitments1) sending sendQueue + goto(SHUTDOWN) using d.copy(commitments = commitments1, localShutdown = shutdown) sending sendQueue } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 4141a076af..ad70162e3d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -36,7 +36,7 @@ import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -1023,10 +1023,14 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.forward(bob, channelReestablishAlice) bob2alice.forward(alice, channelReestablishBob) // They retransmit shutdown. - alice2bob.expectMsgType[Shutdown] - alice2bob.forward(bob) - bob2alice.expectMsgType[Shutdown] - bob2alice.forward(alice) + val shutdownAlice = alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob, shutdownAlice) + val shutdownBob = bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice, shutdownBob) + Seq(shutdownAlice, shutdownBob).foreach(shutdown => commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(shutdown.closeeNonce_opt.isEmpty) + case _: TaprootCommitmentFormat => assert(shutdown.closeeNonce_opt.nonEmpty) + }) // They resume HTLC settlement. fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) @@ -1035,6 +1039,14 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit crossSign(bob, alice, bob2alice, alice2bob) awaitCond(alice.stateName == NEGOTIATING_SIMPLE) awaitCond(bob.stateName == NEGOTIATING_SIMPLE) + // They can now sign the closing transaction. + val closingCompleteAlice = alice2bob.expectMsgType[ClosingComplete] + alice2bob.forward(bob, closingCompleteAlice) + bob2alice.expectMsgType[ClosingComplete] // ignored + val closingTx = bob2blockchain.expectMsgType[PublishFinalTx] + val closingSigBob = bob2alice.expectMsgType[ClosingSig] + bob2alice.forward(alice, closingSigBob) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid) } test("recv INPUT_RESTORED", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>