Skip to content

Commit

Permalink
Watch funding txs as soon as published
Browse files Browse the repository at this point in the history
This aligns with what is done in the single-funded case and lets us detect
channel force-close while offline.
  • Loading branch information
t-bast committed Aug 18, 2022
1 parent 075a606 commit bd47992
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,6 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
}
final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments,
shortIds: ShortIds,
otherFundingTxs: Seq[DualFundingTx],
lastSent: ChannelReady) extends PersistentChannelData

final case class DATA_NORMAL(commitments: Commitments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
}

case funding: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
(funding.commitments +: funding.previousFundingTxs.map(_.commitments)).foreach(c => watchFundingTx(c))
// we make sure that the funding tx with the highest feerate has been published
// NB: with dual-funding, we only watch the funding tx once it has been confirmed
publishFundingTx(funding.fundingParams, funding.fundingTx)
goto(OFFLINE) using funding

Expand Down Expand Up @@ -1088,10 +1088,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
d.alternativeCommitments.find(_.commitInput.outPoint.txid == w.tx.txid) match {
case Some(alternativeCommitments) =>
log.info("an alternative funding tx with txid={} got confirmed", w.tx.txid)
val commitTx = alternativeCommitments.fullySignedLocalCommitTx(keyManager).tx
val commitTxs = Set(commitTx.txid, alternativeCommitments.remoteCommit.txid)
blockchain ! WatchFundingSpent(self, alternativeCommitments.commitInput.outPoint.txid, alternativeCommitments.commitInput.outPoint.index.toInt, commitTxs)
watchFundingTx(alternativeCommitments)
context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx))
val commitTx = alternativeCommitments.fullySignedLocalCommitTx(keyManager).tx
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, alternativeCommitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf)
stay() using d.copy(commitments = alternativeCommitments, localCommitPublished = Some(localCommitPublished)) storing() calling doPublish(localCommitPublished, alternativeCommitments)
case None =>
Expand Down Expand Up @@ -1127,6 +1126,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
} else if (d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid)) {
// counterparty may attempt to spend its last commit tx at any time
handleRemoteSpentNext(tx, d)
} else if (d.alternativeCommitments.exists(c => c.remoteCommit.txid == tx.txid || c.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid))) {
// counterparty may attempt to spend an alternative unconfirmed funding tx at any time
handleRemoteSpentAlternative(tx, d.alternativeCommitments, d)
} else {
// counterparty may attempt to spend a revoked commit tx at any time
handleRemoteSpentOther(tx, d)
Expand Down Expand Up @@ -1320,6 +1322,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val

case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d)

case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) if d.previousFundingTxs.map(_.commitments).exists(c => c.remoteCommit.txid == tx.txid || c.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid)) => handleRemoteSpentAlternative(tx, d.previousFundingTxs.map(_.commitments).toList, d)

case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) => handleRemoteSpentFuture(tx, d)

case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d)
Expand Down Expand Up @@ -1515,13 +1519,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val

case Event(WatchFundingDeeplyBuriedTriggered(_, _, _), _) => stay()

case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) =>
handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d))
case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d))

case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)

case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d)

case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) if d.previousFundingTxs.map(_.commitments).exists(c => c.remoteCommit.txid == tx.txid || c.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid)) => handleRemoteSpentAlternative(tx, d.previousFundingTxs.map(_.commitments).toList, d)

case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d)

case Event(e: Error, d: PersistentChannelData) => handleRemoteError(e, d)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package fr.acinq.eclair.channel.fsm

import akka.actor.ActorRef
import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter}
import fr.acinq.bitcoin.ScriptFlags
import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script, Transaction}
Expand Down Expand Up @@ -309,6 +308,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
case InteractiveTxBuilder.SendMessage(msg) => stay() sending msg
case InteractiveTxBuilder.Succeeded(fundingParams, fundingTx, commitments) =>
d.deferred.foreach(self ! _)
watchFundingTx(commitments)
Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams) match {
case Some(fundingMinDepth) =>
blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, fundingMinDepth)
Expand All @@ -318,10 +318,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
case fundingTx: FullySignedSharedTransaction => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using nextData storing() sending fundingTx.localSigs calling publishFundingTx(fundingParams, fundingTx)
}
case None =>
val commitTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid)
blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, commitTxs)
val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown)
val nextData = DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, Nil, channelReady)
val nextData = DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, channelReady)
fundingTx match {
case fundingTx: PartiallySignedSharedTransaction => goto(WAIT_FOR_DUAL_FUNDING_READY) using nextData storing() sending Seq(fundingTx.localSigs, channelReady)
case fundingTx: FullySignedSharedTransaction => goto(WAIT_FOR_DUAL_FUNDING_READY) using nextData storing() sending Seq(fundingTx.localSigs, channelReady) calling publishFundingTx(fundingParams, fundingTx)
Expand Down Expand Up @@ -395,14 +393,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(confirmedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match {
case Success(_) =>
log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex with funding txid=${commitments.commitInput.outPoint.txid}")
val commitTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid)
blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, commitTxs)
watchFundingTx(commitments)
context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, confirmedTx))
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt))
val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus)
d.deferred.foreach(self ! _)
val otherFundingTxs = allFundingTxs.filter(_.commitments.commitInput.outPoint.txid != confirmedTx.txid)
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, otherFundingTxs, channelReady) storing() sending channelReady
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, channelReady) storing() sending channelReady
case Failure(t) =>
log.error(t, s"rejecting channel with invalid funding tx: ${confirmedTx.bin}")
allFundingTxs.foreach(f => wallet.rollback(f.fundingTx.tx.buildUnsignedTx()))
Expand All @@ -427,12 +423,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
(d.fundingParams.remoteAmount == 0.sat || d.commitments.localParams.initFeatures.hasFeature(Features.ZeroConf))
if (canUseZeroConf) {
log.info("this chanel isn't zero-conf, but they sent an early channel_ready with an alias: no need to wait for confirmations")
val commitTxs = Set(d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, d.commitments.remoteCommit.txid)
blockchain ! WatchFundingSpent(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.outPoint.index.toInt, commitTxs)
val (shortIds, localChannelReady) = acceptFundingTx(d.commitments, RealScidStatus.Unknown)
self ! remoteChannelReady
// NB: we will receive a WatchFundingConfirmedTriggered later that will simply be ignored
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(d.commitments, shortIds, Nil, localChannelReady) storing() sending localChannelReady
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(d.commitments, shortIds, localChannelReady) storing() sending localChannelReady
} else {
log.info("received their channel_ready, deferring message")
stay() using d.copy(deferred = Some(remoteChannelReady)) // no need to store, they will re-send if we get disconnected
Expand All @@ -445,6 +439,19 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs)
stay()

case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
// We wait for one of the funding transactions to confirm before going to the closing state, as the spent funding
// tx and the associated commit tx could be replaced by a new version of the funding tx.
if (tx.txid == d.commitments.remoteCommit.txid) {
log.warning("funding tx spent by txid={} while still unconfirmed", tx.txid)
stay()
} else if (d.previousFundingTxs.exists(_.commitments.remoteCommit.txid == tx.txid)) {
log.warning("previous funding tx spent by txid while still unconfirmed", tx.txid)
stay()
} else {
handleInformationLeak(tx, d)
}

case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleRemoteError(e, d)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import akka.actor.{ActorRef, FSM}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction}
import fr.acinq.eclair.NotificationsLogger
import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchOutputSpent, WatchTxConfirmed}
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy
Expand Down Expand Up @@ -281,6 +281,24 @@ trait ErrorHandlers extends CommonHandlers {
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments)
}

def handleRemoteSpentAlternative(commitTx: Transaction, alternativeCommitments: List[Commitments], d: PersistentChannelData) = {
val commitments_opt = alternativeCommitments.find(c => c.remoteCommit.txid == commitTx.txid || c.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == commitTx.txid))
require(commitments_opt.nonEmpty, "there should be a commit spending an alternative funding tx matching this transaction")
val commitments = commitments_opt.get
log.warning("they published their commit with txid={} spending an alternative funding tx with fundingTxid={}", commitTx.txid, commitments.commitInput.outPoint.txid)

context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput, commitTx, commitments.localParams.isInitiator), "remote-commit"))
// We wait for this alternative funding tx to be confirmed before claiming the commit tx outputs, as it could still
// be replaced by another funding tx.
blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks)
val nextData = d match {
case waitForFundingConfirmed: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, waitForFundingConfirmed.signedFundingTx_opt, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments, mutualCloseProposed = Nil)
case closing: DATA_CLOSING => closing
case _ => DATA_CLOSING(d.commitments, None, waitingSince = nodeParams.currentBlockHeight, alternativeCommitments, mutualCloseProposed = Nil)
}
goto(CLOSING) using nextData storing()
}

def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = {
import remoteCommitPublished._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,6 @@ private[channel] object ChannelCodecs3 {
val DATA_WAIT_FOR_DUAL_FUNDING_READY_0c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = (
("commitments" | commitmentsCodec) ::
("shortIds" | shortids) ::
("otherFundingTxs" | seqOfN(uint16, dualFundingTxCodec)) ::
("lastSent" | lengthDelimited(channelReadyCodec))).as[DATA_WAIT_FOR_DUAL_FUNDING_READY]

val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,18 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
alice2bob.expectMsgType[TxSignatures]
alice2bob.forward(bob)
val fundingTx = eventListener.expectMsgType[TransactionPublished].tx
alice2blockchain.expectMsgType[WatchFundingSpent]
bob2blockchain.expectMsgType[WatchFundingSpent]
if (!channelType.features.contains(Features.ZeroConf)) {
eventually(assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED))
eventually(assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED))
alice2blockchain.expectMsgType[WatchFundingConfirmed]
bob2blockchain.expectMsgType[WatchFundingConfirmed]
alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
alice2blockchain.expectMsgType[WatchFundingSpent]
bob2blockchain.expectMsgType[WatchFundingSpent]
}
alice2blockchain.expectMsgType[WatchFundingSpent]
bob2blockchain.expectMsgType[WatchFundingSpent]
alice2blockchain.expectMsgType[WatchFundingLost]
bob2blockchain.expectMsgType[WatchFundingLost]
alice2bob.expectMsgType[ChannelReady]
Expand Down
Loading

0 comments on commit bd47992

Please sign in to comment.