From b1fb1e9e4a10b44cec242eb9739aef85dc26e2e5 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:47:36 +0200 Subject: [PATCH] Ignore `commit_sig` for aborted splice (#508) After exchanging `tx_complete`, we validate the splice transaction before sending our `commit_sig`. If we consider the transaction invalid, we send `tx_abort`. But if our peer thinks the transaction is valid, they will send their `commit_sig`, which we must ignore until they've acked our `tx_abort`. --- .../acinq/lightning/channel/states/Normal.kt | 7 +++- .../channel/states/SpliceTestsCommon.kt | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index c82be22d3..0868131fd 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -173,6 +173,10 @@ data class Normal( is Either.Right -> Pair(this@Normal.copy(commitments = result.value), listOf()) } is CommitSig -> when { + spliceStatus == SpliceStatus.Aborted -> { + logger.warning { "received commit_sig after sending tx_abort, they probably sent it before receiving our tx_abort, ignoring..." } + Pair(this@Normal, listOf()) + } spliceStatus is SpliceStatus.WaitingForSigs -> { val (signingSession1, action) = spliceStatus.session.receiveCommitSig(channelKeys(), commitments.params, cmd.message, currentBlockHeight.toLong()) when (action) { @@ -530,7 +534,8 @@ data class Normal( } else -> when (commitments.latest.localFundingStatus) { is LocalFundingStatus.UnconfirmedFundingTx -> when (commitments.latest.localFundingStatus.sharedTx) { - is PartiallySignedSharedTransaction -> when (val fullySignedTx = commitments.latest.localFundingStatus.sharedTx.addRemoteSigs(channelKeys(), commitments.latest.localFundingStatus.fundingParams, cmd.message)) { + is PartiallySignedSharedTransaction -> when (val fullySignedTx = + commitments.latest.localFundingStatus.sharedTx.addRemoteSigs(channelKeys(), commitments.latest.localFundingStatus.fundingParams, cmd.message)) { null -> { logger.warning { "received invalid remote funding signatures for txId=${cmd.message.txId}" } logger.warning { "tx=${commitments.latest.localFundingStatus.sharedTx}" } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 91552ff52..72e6e1922 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -136,6 +136,39 @@ class SpliceTestsCommon : LightningTestSuite() { } } + @Test + fun `abort after tx_complete then receive commit_sig`() { + val cmd = createSpliceOutRequest(50_000.sat) + val (alice, bob) = reachNormal() + val (alice1, actionsAlice1) = alice.process(cmd) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(actionsAlice1.findOutgoingMessage())) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob1.findOutgoingMessage())) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + val txOut1 = actionsAlice3.findOutgoingMessage() + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(txOut1)) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + // Instead of relaying the second output, we duplicate the first one, which will make Bob abort after receiving tx_complete. + actionsAlice4.hasOutgoingMessage() + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(txOut1.copy(serialId = 100))) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + val commitSigAlice = actionsAlice5.findOutgoingMessage() + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) + val txAbortBob = actionsBob5.findOutgoingMessage() + val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(txAbortBob)) + assertIs(alice6.state) + assertEquals(1, alice6.commitments.active.size) + assertEquals(SpliceStatus.None, alice6.state.spliceStatus) + val txAbortAlice = actionsAlice6.findOutgoingMessage() + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(commitSigAlice)) + assertTrue(actionsBob6.isEmpty()) + val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(txAbortAlice)) + assertIs(bob7.state) + assertEquals(1, bob7.commitments.active.size) + assertEquals(SpliceStatus.None, bob7.state.spliceStatus) + assertTrue(actionsBob7.isEmpty()) + } + @Test fun `exchange splice_locked`() { val (alice, bob) = reachNormal()