From 93aeaa86bc9049162c6e32541ec25bac1aedf170 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 19 Aug 2022 15:27:37 +0200 Subject: [PATCH 1/3] Unlock utxos during dual funding failures When an alternative funding transaction confirms, we need to unlock other candidates: we may not have published them yet if for example we didn't receive remote signatures. --- .../fr/acinq/eclair/channel/ChannelData.scala | 21 ++++++++++++------- .../eclair/channel/InteractiveTxBuilder.scala | 6 +++++- .../fr/acinq/eclair/channel/fsm/Channel.scala | 15 ++++++++----- .../channel/fsm/ChannelOpenDualFunded.scala | 3 +-- .../channel/fsm/DualFundingHandlers.scala | 18 +++++++++++++++- .../eclair/channel/fsm/ErrorHandlers.scala | 8 +++---- .../channel/version0/ChannelCodecs0.scala | 4 ++-- .../channel/version1/ChannelCodecs1.scala | 2 +- .../channel/version2/ChannelCodecs2.scala | 2 +- .../channel/version3/ChannelCodecs3.scala | 10 ++++++--- 10 files changed, 61 insertions(+), 28 deletions(-) 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 af2d8fd2b9..f7b85b7cf5 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 @@ -400,6 +400,16 @@ object RealScidStatus { */ case class ShortIds(real: RealScidStatus, localAlias: Alias, remoteAlias_opt: Option[Alias]) +sealed trait UnconfirmedFundingTx { + def signedTx_opt: Option[Transaction] +} +case class SingleFundedUnconfirmedFundingTx(signedTx: Transaction) extends UnconfirmedFundingTx { + override val signedTx_opt: Option[Transaction] = Some(signedTx) +} +case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction) extends UnconfirmedFundingTx { + override def signedTx_opt: Option[Transaction] = sharedTx.signedTx_opt +} + /** Once a dual funding tx has been signed, we must remember the associated commitments. */ case class DualFundingTx(fundingTx: SignedSharedTransaction, commitments: Commitments) @@ -486,12 +496,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm lastChecked: BlockHeight, // last time we checked if the channel was double-spent rbfAttempt: Option[typed.ActorRef[InteractiveTxBuilder.Command]], - deferred: Option[ChannelReady]) extends PersistentChannelData { - val signedFundingTx_opt: Option[Transaction] = fundingTx match { - case _: PartiallySignedSharedTransaction => None - case tx: FullySignedSharedTransaction => Some(tx.signedTx) - } -} + deferred: Option[ChannelReady]) extends PersistentChannelData final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, shortIds: ShortIds, lastSent: ChannelReady) extends PersistentChannelData @@ -512,9 +517,9 @@ final case class DATA_NEGOTIATING(commitments: Commitments, require(!commitments.localParams.isInitiator || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing") } final case class DATA_CLOSING(commitments: Commitments, - fundingTx: Option[Transaction], // this will be non-empty if we are the initiator and we got in closing while waiting for our own tx to be published + fundingTx: Option[UnconfirmedFundingTx], waitingSince: BlockHeight, // how long since we initiated the closing - alternativeCommitments: List[Commitments], // commitments we signed that spend a different funding output + alternativeCommitments: List[DualFundingTx], // commitments we signed that spend a different funding output mutualCloseProposed: List[ClosingTx], // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have mutualClosePublished: List[ClosingTx] = Nil, localCommitPublished: Option[LocalCommitPublished] = None, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala index 219f52b64f..a1c0c35721 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala @@ -167,8 +167,11 @@ object InteractiveTxBuilder { sealed trait SignedSharedTransaction { def tx: SharedTransaction def localSigs: TxSignatures + def signedTx_opt: Option[Transaction] + } + case class PartiallySignedSharedTransaction(tx: SharedTransaction, localSigs: TxSignatures) extends SignedSharedTransaction { + override val signedTx_opt: Option[Transaction] = None } - case class PartiallySignedSharedTransaction(tx: SharedTransaction, localSigs: TxSignatures) extends SignedSharedTransaction case class FullySignedSharedTransaction(tx: SharedTransaction, localSigs: TxSignatures, remoteSigs: TxSignatures) extends SignedSharedTransaction { val signedTx: Transaction = { import tx._ @@ -182,6 +185,7 @@ object InteractiveTxBuilder { val outputs = (localTxOut ++ remoteTxOut).sortBy(_._1).map(_._2) Transaction(2, inputs, outputs, lockTime) } + override val signedTx_opt: Option[Transaction] = Some(signedTx) val feerate: FeeratePerKw = Transactions.fee2rate(tx.fees, signedTx.weight()) } // @formatter:on 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 31dadfbb0c..3b12417ff2 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 @@ -264,7 +264,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // There are unconfirmed, alternative funding transactions, so we wait for one to confirm before // watching transactions spending it. blockchain ! WatchFundingConfirmed(self, data.commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - closing.alternativeCommitments.foreach(c => blockchain ! WatchFundingConfirmed(self, c.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks)) + closing.alternativeCommitments.foreach(c => blockchain ! WatchFundingConfirmed(self, c.commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks)) } closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isInitiator)) closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments)) @@ -276,7 +276,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // if commitment number is zero, we also need to make sure that the funding tx has been published if (closing.commitments.localCommit.index == 0 && closing.commitments.remoteCommit.index == 0) { if (closing.commitments.channelFeatures.hasFeature(Features.DualFunding)) { - closing.fundingTx.foreach(tx => wallet.publishTransaction(tx)) + closing.fundingTx.flatMap(_.signedTx_opt).foreach(tx => wallet.publishTransaction(tx)) } else { blockchain ! GetTxWithMeta(self, closing.commitments.commitInput.outPoint.txid) } @@ -1086,15 +1086,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // NB: waitingSinceBlock contains the block at which closing was initiated, not the block at which funding was initiated. // That means we're lenient with our peer and give its funding tx more time to confirm, to avoid having to store two distinct // waitingSinceBlock (e.g. closingWaitingSinceBlock and fundingWaitingSinceBlock). - handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx.flatMap(_.signedTx_opt)) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_CLOSING) => handleFundingPublishFailed(d) case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_CLOSING) => handleFundingTimeout(d) case Event(w: WatchFundingConfirmedTriggered, d: DATA_CLOSING) => - d.alternativeCommitments.find(_.commitInput.outPoint.txid == w.tx.txid) match { - case Some(commitments1) => + d.alternativeCommitments.find(_.commitments.commitInput.outPoint.txid == w.tx.txid) match { + case Some(DualFundingTx(_, commitments1)) => // This is a corner case where: // - we are using dual funding // - *and* the funding tx was RBF-ed @@ -1110,6 +1110,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val log.info("channelId={} was confirmed at blockHeight={} txIndex={} with a previous funding txid={}", d.channelId, w.blockHeight, w.txIndex, w.tx.txid) watchFundingTx(commitments1) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) + val otherFundingTxs = d.fundingTx match { + case Some(DualFundedUnconfirmedFundingTx(fundingTx)) => fundingTx +: d.alternativeCommitments.filter(_.commitments.commitInput.outPoint.txid != w.tx.txid).map(_.fundingTx) + case _ => d.alternativeCommitments.filter(_.commitments.commitInput.outPoint.txid != w.tx.txid).map(_.fundingTx) + } + rollbackDualFundingTxs(otherFundingTxs) val commitTx = commitments1.fullySignedLocalCommitTx(keyManager).tx val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, commitments1, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) val d1 = DATA_CLOSING(commitments1, None, d.waitingSince, alternativeCommitments = Nil, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index d65877f496..5d0e27fb1c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -28,8 +28,6 @@ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Features, RealShortChannelId} -import scala.concurrent.duration.DurationInt - /** * Created by t-bast on 19/04/2022. */ @@ -396,6 +394,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, channelReady) storing() sending channelReady case None => log.error(s"internal error: the funding tx that confirmed doesn't match any of our funding txs: ${confirmedTx.bin}") + rollbackDualFundingTxs(allFundingTxs.map(_.fundingTx)) goto(CLOSED) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index 8037f6cc64..739b2a611e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -16,10 +16,11 @@ package fr.acinq.eclair.channel.fsm +import fr.acinq.bitcoin.scalacompat.{Transaction, TxIn} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingConfirmedTriggered import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, SignedSharedTransaction} +import fr.acinq.eclair.channel.InteractiveTxBuilder._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.BITCOIN_FUNDING_DOUBLE_SPENT import fr.acinq.eclair.wire.protocol.Error @@ -61,6 +62,7 @@ trait DualFundingHandlers extends CommonFundingHandlers { watchFundingTx(d.commitments) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) // We can forget previous funding attempts now that the funding tx is confirmed. + rollbackDualFundingTxs(d.previousFundingTxs.map(_.fundingTx)) stay() using d.copy(previousFundingTxs = Nil) storing() } else if (d.previousFundingTxs.exists(_.commitments.commitInput.outPoint.txid == w.tx.txid)) { log.info("channelId={} was confirmed at blockHeight={} txIndex={} with a previous funding txid={}", d.channelId, w.blockHeight, w.txIndex, w.tx.txid) @@ -68,6 +70,8 @@ trait DualFundingHandlers extends CommonFundingHandlers { watchFundingTx(confirmed.commitments) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) // We can forget other funding attempts now that one of the funding txs is confirmed. + val otherFundingTxs = d.fundingTx +: d.previousFundingTxs.filter(_.commitments.commitInput.outPoint.txid != w.tx.txid).map(_.fundingTx) + rollbackDualFundingTxs(otherFundingTxs) stay() using d.copy(commitments = confirmed.commitments, fundingTx = confirmed.fundingTx, previousFundingTxs = Nil) storing() } else { log.error(s"internal error: a funding tx confirmed that doesn't match any of our funding txs: ${w.tx.txid}") @@ -109,4 +113,16 @@ trait DualFundingHandlers extends CommonFundingHandlers { } } + /** + * In most cases we don't need to explicitly rollback funding transactions, as the locks are automatically removed by + * bitcoind when transactions are published. But if we didn't publish those transactions (e.g. because our peer never + * sent us their signatures), their inputs may still be locked. + */ + def rollbackDualFundingTxs(txs: Seq[SignedSharedTransaction]): Unit = { + val inputs = txs.flatMap(_.tx.localInputs).distinctBy(_.serialId).map(i => TxIn(toOutPoint(i), Nil, 0)) + if (inputs.nonEmpty) { + wallet.rollback(Transaction(2, inputs, Nil, 0)) + } + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index f89b340520..0150e29e21 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -196,8 +196,8 @@ trait ErrorHandlers extends CommonHandlers { val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.signedFundingTx_opt, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = waitForFundingConfirmed.previousFundingTxs.map(_.commitments), mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = Some(DualFundedUnconfirmedFundingTx(waitForFundingConfirmed.fundingTx)), waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = waitForFundingConfirmed.previousFundingTxs, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) } goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) @@ -242,8 +242,8 @@ trait ErrorHandlers extends CommonHandlers { val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.signedFundingTx_opt, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = waitForFundingConfirmed.previousFundingTxs.map(_.commitments), mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = Some(DualFundedUnconfirmedFundingTx(waitForFundingConfirmed.fundingTx)), waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = waitForFundingConfirmed.previousFundingTxs, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments = Nil, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) } goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 9acb5c8bd6..3dee0bfe5d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -435,7 +435,7 @@ private[channel] object ChannelCodecs0 { ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - DATA_CLOSING(commitments, fundingTx, waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) + DATA_CLOSING(commitments, fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly val DATA_CLOSING_09_Codec: Codec[DATA_CLOSING] = ( @@ -450,7 +450,7 @@ private[channel] object ChannelCodecs0 { ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - DATA_CLOSING(commitments, fundingTx, waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) + DATA_CLOSING(commitments, fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly val channelReestablishCodec: Codec[ChannelReestablish] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index e6cc8fe002..42af4f6de2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -285,7 +285,7 @@ private[channel] object ChannelCodecs1 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - DATA_CLOSING(commitments, fundingTx, waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) + DATA_CLOSING(commitments, fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index bfeb9ee0b3..110e876948 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -320,7 +320,7 @@ private[channel] object ChannelCodecs2 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - DATA_CLOSING(commitments, fundingTx, waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) + DATA_CLOSING(commitments, fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 8647df564b..d5406c09dc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -464,14 +464,18 @@ private[channel] object ChannelCodecs3 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - DATA_CLOSING(commitments, fundingTx, waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) + DATA_CLOSING(commitments, fundingTx.map(tx => SingleFundedUnconfirmedFundingTx(tx)), waitingSince, Nil, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly + val unconfirmedFundingTxCodec: Codec[UnconfirmedFundingTx] = discriminated[UnconfirmedFundingTx].by(uint8) + .typecase(0x01, txCodec.as[SingleFundedUnconfirmedFundingTx]) + .typecase(0x02, signedSharedTransactionCodec.as[DualFundedUnconfirmedFundingTx]) + val DATA_CLOSING_0d_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: - ("fundingTx" | optional(bool8, txCodec)) :: + ("fundingTx" | optional(bool8, unconfirmedFundingTxCodec)) :: ("waitingSince" | blockHeight) :: - ("alternativeCommitments" | listOfN(uint16, commitmentsCodec)) :: + ("alternativeCommitments" | listOfN(uint16, dualFundingTxCodec)) :: ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: From 5eda6bd4fff10fd8ee0d68f5f20addb34901d343 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 19 Aug 2022 17:13:49 +0200 Subject: [PATCH 2/3] fixup! Unlock utxos during dual funding failures --- .../main/scala/fr/acinq/eclair/channel/fsm/Channel.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 3b12417ff2..24371220e8 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 @@ -1110,10 +1110,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val log.info("channelId={} was confirmed at blockHeight={} txIndex={} with a previous funding txid={}", d.channelId, w.blockHeight, w.txIndex, w.tx.txid) watchFundingTx(commitments1) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) - val otherFundingTxs = d.fundingTx match { - case Some(DualFundedUnconfirmedFundingTx(fundingTx)) => fundingTx +: d.alternativeCommitments.filter(_.commitments.commitInput.outPoint.txid != w.tx.txid).map(_.fundingTx) - case _ => d.alternativeCommitments.filter(_.commitments.commitInput.outPoint.txid != w.tx.txid).map(_.fundingTx) - } + val otherFundingTxs = + d.fundingTx.toSeq.collect { case DualFundedUnconfirmedFundingTx(fundingTx) => fundingTx } ++ + d.alternativeCommitments.filterNot(_.commitments.commitInput.outPoint.txid == w.tx.txid).map(_.fundingTx) rollbackDualFundingTxs(otherFundingTxs) val commitTx = commitments1.fullySignedLocalCommitTx(keyManager).tx val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, commitments1, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) From 764ec6d10e5da2faf10a20e9eacdb3f2b4940cae Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 19 Aug 2022 17:17:36 +0200 Subject: [PATCH 3/3] Clarify comment --- .../fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index 739b2a611e..0bf77b2442 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -115,8 +115,8 @@ trait DualFundingHandlers extends CommonFundingHandlers { /** * In most cases we don't need to explicitly rollback funding transactions, as the locks are automatically removed by - * bitcoind when transactions are published. But if we didn't publish those transactions (e.g. because our peer never - * sent us their signatures), their inputs may still be locked. + * bitcoind when transactions are published. But if we couldn't publish those transactions (e.g. because our peer + * never sent us their signatures, or the transaction wasn't accepted in our mempool), their inputs may still be locked. */ def rollbackDualFundingTxs(txs: Seq[SignedSharedTransaction]): Unit = { val inputs = txs.flatMap(_.tx.localInputs).distinctBy(_.serialId).map(i => TxIn(toOutPoint(i), Nil, 0))