Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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],
Copy link
Copy Markdown
Member Author

@t-bast t-bast Aug 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new trait is a bit verbose, but it highlights the fact that this is only used when the funding tx is unconfirmed (which wasn't obvious before). Alternatively, we could just use an Option[Either[Transaction, SignedSharedTransaction]], let me know what you prefer.

What is still a bit confusing is that in the single-funded case, this can be None for two very different reasons:

  1. The funding tx is confirmed
  2. The funding tx is unconfirmed but we're fundee so we don't have it

The dual-funded case doesn't have this ambiguity. I don't know if it's worth fixing for the single-funded case, it would mean wrapping another Option in the SingleFundedUnconfirmedFundingTx case class.

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -1110,6 +1110,10 @@ 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.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)
val d1 = DATA_CLOSING(commitments1, None, d.waitingSince, alternativeCommitments = Nil, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,13 +62,16 @@ 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)
val confirmed = d.previousFundingTxs.find(_.commitments.commitInput.outPoint.txid == w.tx.txid).get
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}")
Expand Down Expand Up @@ -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 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 = {
Comment thread
pm47 marked this conversation as resolved.
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))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] = (
Expand All @@ -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] = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] = (
Expand Down
Loading