diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 6b519059e7..ffd4b13c68 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -536,36 +536,18 @@ object SpliceStatus { case object SpliceAborted extends SpliceStatus } -case class ClosingCompleteSent(closingComplete: ClosingComplete, closingFeerate: FeeratePerKw) - -sealed trait OnRemoteShutdown -object OnRemoteShutdown { - /** When receiving the remote shutdown, we sign a new version of our closing transaction. */ - case class SignTransaction(closingFeerate: FeeratePerKw) extends OnRemoteShutdown - /** When receiving the remote shutdown, we don't sign a new version of our closing transaction, but our peer may sign theirs. */ - case object WaitForSigs extends OnRemoteShutdown -} - sealed trait ClosingNegotiation { def localShutdown: Shutdown - // When we disconnect, we discard pending signatures. - def disconnect(): ClosingNegotiation.WaitingForRemoteShutdown = this match { - case status: ClosingNegotiation.WaitingForRemoteShutdown => status - case status: ClosingNegotiation.SigningTransactions => status.closingCompleteSent_opt.map(_.closingFeerate) match { - // If we were waiting for their signature, we will send closing_complete again after exchanging shutdown. - case Some(closingFeerate) if status.closingSigReceived_opt.isEmpty => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate)) - case _ => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.WaitForSigs) - } - case status: ClosingNegotiation.WaitingForConfirmation => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.WaitForSigs) - } + /** Closing feerate for our closing transaction. */ + def closingFeerate: FeeratePerKw } object ClosingNegotiation { /** We've sent a new shutdown message: we wait for their shutdown message before taking any action. */ - case class WaitingForRemoteShutdown(localShutdown: Shutdown, onRemoteShutdown: OnRemoteShutdown) extends ClosingNegotiation - /** We've exchanged shutdown messages: at least one side will send closing_complete to renew their closing transaction. */ - case class SigningTransactions(localShutdown: Shutdown, remoteShutdown: Shutdown, closingCompleteSent_opt: Option[ClosingCompleteSent], closingSigSent_opt: Option[ClosingSig], closingSigReceived_opt: Option[ClosingSig]) extends ClosingNegotiation - /** We've signed a new closing transaction and are waiting for confirmation or to initiate RBF. */ - case class WaitingForConfirmation(localShutdown: Shutdown, remoteShutdown: Shutdown) extends ClosingNegotiation + case class WaitingForRemoteShutdown(localShutdown: Shutdown, closingFeerate: FeeratePerKw) extends ClosingNegotiation + /** We've exchanged shutdown messages: we both send closing_complete to renew the closing transactions. */ + case class SigningTransactions(localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerate: FeeratePerKw, closingCompleteSent_opt: Option[ClosingComplete], closingSigSent_opt: Option[ClosingSig], closingSigReceived_opt: Option[ClosingSig]) extends ClosingNegotiation + /** We've signed new closing transactions and are waiting for confirmation or to initiate RBF. */ + case class WaitingForConfirmation(localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerate: FeeratePerKw) extends ClosingNegotiation } sealed trait ChannelData extends PossiblyHarmful { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index ad2ef0366b..12dddeb955 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -117,6 +117,7 @@ case class InvalidAnnouncementSignatures (override val channelId: Byte case class InvalidCommitmentSignature (override val channelId: ByteVector32, fundingTxId: TxId, fundingTxIndex: Long, unsignedCommitTx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: fundingTxId=$fundingTxId fundingTxIndex=$fundingTxIndex commitTxId=${unsignedCommitTx.txid} commitTx=$unsignedCommitTx") case class InvalidHtlcSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid htlc signature: txId=$txId") case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed") +case class ShutdownWaitingForSigs (override val channelId: ByteVector32) extends ChannelException(channelId, "received unexpected shutdown while signing closing transactions") case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output") case class InvalidCloseSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid close signature: txId=$txId") case class UnexpectedClosingComplete (override val channelId: ByteVector32, fees: Satoshi, lockTime: Long) extends ChannelException(channelId, s"unexpected closing_complete with fees=$fees and lockTime=$lockTime: we already sent closing_sig, you must send shutdown first") 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 a2ae4242f6..fe7ac214d0 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 @@ -1727,77 +1727,69 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with when(NEGOTIATING_SIMPLE)(handleExceptions { case Event(remoteShutdown: Shutdown, d: DATA_NEGOTIATING_SIMPLE) => + val localScript = d.status.localShutdown.scriptPubKey + val remoteScript = remoteShutdown.scriptPubKey d.status match { case status: ClosingNegotiation.WaitingForRemoteShutdown => // We have already sent our shutdown. Now that we've received theirs, we're ready to sign closing transactions. - // If we don't have a closing feerate, we don't need to create a new version of our closing transaction (which - // can happen after a reconnection for example). - status.onRemoteShutdown match { - case OnRemoteShutdown.SignTransaction(closingFeerate) => - val localScript = status.localShutdown.scriptPubKey - val remoteScript = remoteShutdown.scriptPubKey - MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, remoteScript, closingFeerate) match { - case Left(f) => - log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) - val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, None, None, None) - stay() using d.copy(status = status1) - case Right((closingTxs, closingComplete)) => - log.debug("signing local mutual close transactions: {}", closingTxs) - val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, Some(ClosingCompleteSent(closingComplete, closingFeerate)), None, None) - stay() using d.copy(status = status1, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending closingComplete - } - case OnRemoteShutdown.WaitForSigs => - val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, None, None, None) + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, remoteScript, status.closingFeerate) match { + case Left(f) => + log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) + val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, status.closingFeerate, None, None, None) stay() using d.copy(status = status1) + case Right((closingTxs, closingComplete)) => + log.debug("signing local mutual close transactions: {}", closingTxs) + val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, status.closingFeerate, Some(closingComplete), None, None) + stay() using d.copy(status = status1, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending closingComplete } - case status: ClosingNegotiation.SigningTransactions => - // We were in the middle of signing transactions: we restart a signing round from scratch. - // If we were waiting for their signature, we will send closing_complete again after exchanging shutdown. - val localShutdown = status.localShutdown - val onRemoteShutdown = status.closingCompleteSent_opt.map(_.closingFeerate) match { - case Some(closingFeerate) if status.closingSigReceived_opt.isEmpty => OnRemoteShutdown.SignTransaction(closingFeerate) - case _ => OnRemoteShutdown.WaitForSigs - } - val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, onRemoteShutdown) - self ! remoteShutdown - stay() using d.copy(status = status1) sending localShutdown + case _: ClosingNegotiation.SigningTransactions => + // We were in the middle of signing transactions: sending shutdown is forbidden at that point. + context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) + stay() sending Warning(d.channelId, ShutdownWaitingForSigs(d.channelId).getMessage) case status: ClosingNegotiation.WaitingForConfirmation => // Our peer wants to create a new version of their closing transaction. We don't need to update our version of - // the closing transaction: we re-send our previous shutdown and wait for their closing_complete. + // the closing transaction: we use the same parameters as we did in the previous signing round. val localShutdown = status.localShutdown - val status1 = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None) - stay() using d.copy(status = status1) sending localShutdown + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, remoteScript, status.closingFeerate) match { + case Left(f) => + log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) + val status1 = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, status.closingFeerate, None, None, None) + stay() using d.copy(status = status1) sending localShutdown + case Right((closingTxs, closingComplete)) => + log.debug("signing local mutual close transactions: {}", closingTxs) + val status1 = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, status.closingFeerate, Some(closingComplete), None, None) + stay() using d.copy(status = status1, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending Seq(localShutdown, closingComplete) + } } case Event(closingComplete: ClosingComplete, d: DATA_NEGOTIATING_SIMPLE) => d.status match { case _: ClosingNegotiation.WaitingForRemoteShutdown => log.info("ignoring remote closing_complete, we've sent shutdown to initiate a new signing round") - stay() + stay() sending Warning(d.channelId, UnexpectedClosingComplete(d.channelId, closingComplete.fees, closingComplete.lockTime).getMessage) case _: ClosingNegotiation.WaitingForConfirmation => log.info("ignoring closing_complete, we've already sent closing_sig: peer must send shutdown again before closing_complete") stay() sending Warning(d.channelId, UnexpectedClosingComplete(d.channelId, closingComplete.fees, closingComplete.lockTime).getMessage) case status: ClosingNegotiation.SigningTransactions if status.closingSigSent_opt.nonEmpty => - log.info("ignoring closing_complete, we've already sent closing_sig: peer must send shutdown again before closing_complete") + log.info("ignoring closing_complete, we've already sent closing_sig: peer must send closing_sig, then shutdown again before closing_complete") stay() sending Warning(d.channelId, UnexpectedClosingComplete(d.channelId, closingComplete.fees, closingComplete.lockTime).getMessage) case status: ClosingNegotiation.SigningTransactions => val localScript = status.localShutdown.scriptPubKey val remoteScript = status.remoteShutdown.scriptPubKey MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, localScript, remoteScript, closingComplete) match { case Left(f) => - // This may happen if scripts were updated concurrently, so we simply ignore failures. log.warning("invalid closing_complete: {}", f.getMessage) - stay() + stay() sending Warning(d.channelId, f.getMessage) case Right((signedClosingTx, closingSig)) => log.debug("signing remote mutual close transaction: {}", signedClosingTx.tx) val status1 = status.closingCompleteSent_opt match { // We've sent closing_complete: we may be waiting for their closing_sig. case Some(_) => status.closingSigReceived_opt match { - case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown) + case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown, status.closingFeerate) case None => status.copy(closingSigSent_opt = Some(closingSig)) } - // We haven't sent closing_complete: we're not waiting for their closing_sig'. - case None => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown) + // We haven't sent closing_complete: we're not waiting for their closing_sig. + case None => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown, status.closingFeerate) } val d1 = d.copy(status = status1, publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx) stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = false) sending closingSig @@ -1818,14 +1810,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case status: ClosingNegotiation.SigningTransactions => MutualClose.receiveSimpleClosingSig(keyManager, d.commitments.latest, d.proposedClosingTxs.last, closingSig) match { case Left(f) => - // This may happen if scripts were updated concurrently, so we simply ignore failures. log.warning("invalid closing_sig: {}", f.getMessage) - stay() + stay() sending Warning(d.channelId, f.getMessage) case Right(signedClosingTx) => log.debug("received signatures for local mutual close transaction: {}", signedClosingTx.tx) val status1 = status.closingSigSent_opt match { // We have already signed their transaction: both local and remote closing transactions have been updated. - case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown) + case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown, status.closingFeerate) // We haven't sent closing_sig yet: they may send us closing_complete to update their closing transaction. case None => status.copy(closingSigReceived_opt = Some(closingSig)) } @@ -1859,17 +1850,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("we're already waiting for our peer to send their shutdown message, no need to send ours again") handleCommandError(ClosingAlreadyInProgress(d.channelId), c) case _: ClosingNegotiation.SigningTransactions => - val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate)) - stay() using d.copy(status = status1) storing() sending localShutdown + log.info("we're in the middle of signing closing transactions, we should finish this round before starting a new signing session") + handleCommandError(ClosingAlreadyInProgress(d.channelId), c) case _: ClosingNegotiation.WaitingForConfirmation => - val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate)) + val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, closingFeerate) stay() using d.copy(status = status1) storing() sending localShutdown } case Event(e: Error, d: DATA_NEGOTIATING_SIMPLE) => handleRemoteError(e, d) case Event(INPUT_DISCONNECTED, d: DATA_NEGOTIATING_SIMPLE) => - val status1 = d.status.disconnect() + val status1 = ClosingNegotiation.WaitingForRemoteShutdown(d.status.localShutdown, d.status.closingFeerate) goto(OFFLINE) using d.copy(status = status1) }) @@ -2526,10 +2517,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(_: ChannelReestablish, d: DATA_NEGOTIATING_SIMPLE) => // We retransmit our shutdown: we may have updated our script and they may not have received it. val localShutdown = d.status.localShutdown - val status1 = d.status match { - case status: ClosingNegotiation.WaitingForRemoteShutdown => status.copy(localShutdown = localShutdown) - case _ => ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.WaitForSigs) - } + val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, d.status.closingFeerate) goto(NEGOTIATING_SIMPLE) using d.copy(status = status1) sending localShutdown // This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index d22e0799ea..ab883425dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -139,12 +139,12 @@ trait CommonHandlers { MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) - val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None) + val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, None, None, None) val d = DATA_NEGOTIATING_SIMPLE(commitments, status, Nil, Nil) goto(NEGOTIATING_SIMPLE) using d storing() sending toSend case Right((closingTxs, closingComplete)) => log.debug("signing local mutual close transactions: {}", closingTxs) - val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, Some(ClosingCompleteSent(closingComplete, closingFeerate)), None, None) + val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, Some(closingComplete), None, None) val d = DATA_NEGOTIATING_SIMPLE(commitments, status, closingTxs :: Nil, Nil) goto(NEGOTIATING_SIMPLE) using d storing() sending toSend :+ closingComplete } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala index 083faf417e..53ee59a77b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala @@ -758,20 +758,16 @@ private[channel] object ChannelCodecs4 { ("localAndRemote_opt" | optional(bool8, closingTxCodec)) :: ("localOnly_opt" | optional(bool8, closingTxCodec)) :: ("remoteOnly_opt" | optional(bool8, closingTxCodec))).as[ClosingTxs] - - private val onRemoteShutdownCodec: Codec[OnRemoteShutdown] = discriminated[OnRemoteShutdown].by(uint8) - .typecase(0x00, provide(OnRemoteShutdown.WaitForSigs)) - .typecase(0x01, feeratePerKw.as[OnRemoteShutdown.SignTransaction]) private val waitingForRemoteShutdownCodec: Codec[ClosingNegotiation.WaitingForRemoteShutdown] = ( ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("onRemoteShutdown" | onRemoteShutdownCodec) + ("closingFeerate" | feeratePerKw) ).as[ClosingNegotiation.WaitingForRemoteShutdown] val closingNegotiationCodec: Codec[ClosingNegotiation] = discriminated[ClosingNegotiation].by(uint8) .\(0x01) { case status: ClosingNegotiation.WaitingForRemoteShutdown => status }(waitingForRemoteShutdownCodec) - .\(0x02) { case status: ClosingNegotiation.SigningTransactions => status.disconnect() }(waitingForRemoteShutdownCodec) - .\(0x03) { case status: ClosingNegotiation.WaitingForConfirmation => status.disconnect() }(waitingForRemoteShutdownCodec) + .\(0x02) { case status: ClosingNegotiation.SigningTransactions => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, status.closingFeerate) }(waitingForRemoteShutdownCodec) + .\(0x03) { case status: ClosingNegotiation.WaitingForConfirmation => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, status.closingFeerate) }(waitingForRemoteShutdownCodec) val DATA_NEGOTIATING_SIMPLE_14_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = ( ("commitments" | commitmentsCodec) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index fb91428dca..3aacd874fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -91,7 +91,7 @@ object TestDatabases { // When negotiating closing transactions with the option_simple_close feature, we discard pending signatures on // disconnection and will restart a signing round on reconnection. def freeze4(input: PersistentChannelData): PersistentChannelData = input match { - case d: DATA_NEGOTIATING_SIMPLE => freeze3(d.copy(status = d.status.disconnect())) + case d: DATA_NEGOTIATING_SIMPLE => freeze3(d.copy(status = ClosingNegotiation.WaitingForRemoteShutdown(d.status.localShutdown, d.status.closingFeerate))) case d => freeze3(d) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 174bb245b5..099382197f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.testutils.PimpTestProbe._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat @@ -575,10 +576,12 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.forward(bob, aliceClosingComplete.copy(tlvStream = TlvStream(ClosingTlv.CloserNoClosee(aliceClosingComplete.closerNoCloseeSig_opt.get)))) // Bob expects to receive a signature for a closing transaction containing his output, so he ignores Alice's // closing_complete instead of sending back his closing_sig. + bob2alice.expectMsgType[Warning] bob2alice.expectNoMessage(100 millis) bob2alice.forward(alice, bobClosingComplete) val aliceClosingSig = alice2bob.expectMsgType[ClosingSig] alice2bob.forward(bob, aliceClosingSig.copy(tlvStream = TlvStream(ClosingTlv.NoCloserClosee(aliceClosingSig.closerAndCloseeSig_opt.get)))) + bob2alice.expectMsgType[Warning] bob2alice.expectNoMessage(100 millis) bob2blockchain.expectNoMessage(100 millis) } @@ -586,43 +589,61 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("recv ClosingComplete (with concurrent shutdown)", Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ aliceClose(f) - val aliceClosingComplete1 = alice2bob.expectMsgType[ClosingComplete] // ignored + val aliceClosingComplete1 = alice2bob.expectMsgType[ClosingComplete] bob2alice.expectMsgType[ClosingComplete] bob2alice.forward(alice) - alice2blockchain.expectMsgType[PublishFinalTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] - val aliceClosingSig1 = alice2bob.expectMsgType[ClosingSig] // ignored + val bobTxId1 = alice2blockchain.expectMsgType[PublishFinalTx].tx.txid + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTxId1) + alice2bob.expectMsgType[ClosingSig] + alice2bob.forward(bob) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTxId1) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTxId1) - // Bob updates his closing script before receiving Alice's closing_complete and closing_sig. + // Bob cannot send shutdown while signing is in progress. val bobScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) - bob ! CMD_CLOSE(TestProbe().ref, Some(bobScript), Some(ClosingFeerates(preferred = FeeratePerKw(2500 sat), min = FeeratePerKw(253 sat), max = FeeratePerKw(2500 sat)))) - assert(bob2alice.expectMsgType[Shutdown].scriptPubKey == bobScript) - bob2alice.forward(alice) + val probe = TestProbe() + bob ! CMD_CLOSE(probe.ref, Some(bobScript), Some(ClosingFeerates(preferred = FeeratePerKw(2500 sat), min = FeeratePerKw(253 sat), max = FeeratePerKw(2500 sat)))) + probe.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] - // Bob receives Alice's previous closing_complete and closing_sig and ignores them. + // After sending closing_sig to Alice, Bob can update his closing script. alice2bob.forward(bob, aliceClosingComplete1) - alice2bob.forward(bob, aliceClosingSig1) - bob2alice.expectNoMessage(100 millis) + val bobClosingSig1 = bob2alice.expectMsgType[ClosingSig] + val aliceTxId1 = bob2blockchain.expectMsgType[PublishFinalTx].tx.txid + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId1) + bob ! CMD_CLOSE(probe.ref, Some(bobScript), Some(ClosingFeerates(preferred = FeeratePerKw(2500 sat), min = FeeratePerKw(253 sat), max = FeeratePerKw(2500 sat)))) + val bobShutdown2 = bob2alice.expectMsgType[Shutdown] + assert(bobShutdown2.scriptPubKey == bobScript) + + // If Bob sends shutdown without sending closing_sig first, Alice will ignore it. + bob2alice.forward(alice, bobShutdown2) + alice2bob.expectMsgType[Warning] + alice2bob.expectNoMessage(100 millis) + + // After sending closing_sig, Bob can send his second shutdown. + bob2alice.forward(alice, bobClosingSig1) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTxId1) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId1) + bob2alice.forward(alice, bobShutdown2) // Alice re-sends shutdown in response to Bob's shutdown, at which point they sign transactions from scratch. alice2bob.expectMsgType[Shutdown] alice2bob.forward(bob) bob2alice.expectMsgType[ClosingComplete] bob2alice.forward(alice) - val bobTxId = alice2blockchain.expectMsgType[PublishFinalTx].tx.txid - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTxId) + val bobTxId2 = alice2blockchain.expectMsgType[PublishFinalTx].tx.txid + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTxId2) alice2bob.expectMsgType[ClosingComplete] alice2bob.forward(bob) - val aliceTxId = bob2blockchain.expectMsgType[PublishFinalTx].tx.txid - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId) + val aliceTxId2 = bob2blockchain.expectMsgType[PublishFinalTx].tx.txid + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId2) alice2bob.expectMsgType[ClosingSig] alice2bob.forward(bob) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTxId) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTxId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTxId2) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTxId2) bob2alice.expectMsgType[ClosingSig] bob2alice.forward(alice) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTxId) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTxId2) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId2) } test("recv WatchFundingSpentTriggered (counterparty's mutual close)") { f => @@ -714,8 +735,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.forward(alice) alice2bob.expectMsgType[ClosingComplete] alice2bob.forward(bob) - bob2alice.expectMsgType[ClosingSig] - bob2alice.forward(alice) + bob2alice.expectMsgType[ClosingComplete] // ignored + val bobClosingSig = bob2alice.expectMsgType[ClosingSig] + bob2alice.forward(alice, bobClosingSig) val aliceTx2 = alice2blockchain.expectMsgType[PublishFinalTx].tx alice2blockchain.expectWatchTxConfirmed(aliceTx2.txid) assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx2.txid) @@ -852,10 +874,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.forward(alice) alice2bob.expectMsgType[ClosingComplete] alice2bob.forward(bob) + bob2alice.expectMsgType[ClosingComplete] // ignored val aliceTxId2 = bob2blockchain.expectMsgType[PublishFinalTx].tx.txid assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId2) - bob2alice.expectMsgType[ClosingSig] - bob2alice.forward(alice) + val bobClosingSig = bob2alice.expectMsgType[ClosingSig] + bob2alice.forward(alice, bobClosingSig) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTxId2) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId2) bob2alice.expectNoMessage(100 millis) @@ -906,10 +929,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.forward(alice) alice2bob.expectMsgType[ClosingComplete] alice2bob.forward(bob) + bob2alice.expectMsgType[ClosingComplete] // ignored val aliceTxId2 = bob2blockchain.expectMsgType[PublishFinalTx].tx.txid assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId2) - bob2alice.expectMsgType[ClosingSig] - bob2alice.forward(alice) + val bobClosingSig = bob2alice.expectMsgType[ClosingSig] + bob2alice.forward(alice, bobClosingSig) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTxId2) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTxId2) bob2alice.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index 28a772cf03..6e9007fe02 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -234,23 +234,21 @@ class ChannelCodecs4Spec extends AnyFunSuite { val channelId = randomBytes32() val localShutdown = Shutdown(channelId, Script.write(Script.pay2wpkh(randomKey().publicKey))) val remoteShutdown = Shutdown(channelId, Script.write(Script.pay2wpkh(randomKey().publicKey))) - val waitingForRemoteShutdown = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.WaitForSigs) val closingFeerate = FeeratePerKw(5000 sat) - val waitingForRemoteShutdownWithFeerate = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate)) - val closingCompleteSent = ClosingCompleteSent(ClosingComplete(channelId, 1500 sat, 0), closingFeerate) + val waitingForRemoteShutdown = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, closingFeerate) + val closingComplete = ClosingComplete(channelId, 1500 sat, 0) val closingSigReceived = ClosingSig(channelId) - val testCases = Map( - waitingForRemoteShutdown -> waitingForRemoteShutdown, - waitingForRemoteShutdownWithFeerate -> waitingForRemoteShutdownWithFeerate, - ClosingNegotiation.WaitingForConfirmation(localShutdown, remoteShutdown) -> waitingForRemoteShutdown, - ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None) -> waitingForRemoteShutdown, - ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, Some(closingCompleteSent), None, None) -> waitingForRemoteShutdownWithFeerate, - ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, Some(closingCompleteSent), None, Some(closingSigReceived)) -> waitingForRemoteShutdown, + val testCases = Seq( + waitingForRemoteShutdown, + ClosingNegotiation.WaitingForConfirmation(localShutdown, remoteShutdown, closingFeerate), + ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, None, None, None), + ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, Some(closingComplete), None, None), + ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, Some(closingComplete), None, Some(closingSigReceived)), ) - testCases.foreach { case (status, expected) => + testCases.foreach { status => val encoded = closingNegotiationCodec.encode(status).require val decoded = closingNegotiationCodec.decode(encoded).require.value - assert(decoded == expected) + assert(decoded == waitingForRemoteShutdown) } }