Skip to content

Commit

Permalink
Make updateLocalFundingStatus method return Either (#2602)
Browse files Browse the repository at this point in the history
After calling this method, we perform actions at several places that
only make sense if the correct behavior happened. Instead of assuming
things went ok, we use proper typing and make the result explicit.
  • Loading branch information
pm47 authored Feb 20, 2023
1 parent a3c6029 commit df590d8
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1001,19 +1001,20 @@ case class Commitments(params: ChannelParams,
active.forall(_.commitInput.redeemScript == fundingScript)
}

def updateLocalFundingStatus(txid: ByteVector32, status: LocalFundingStatus)(implicit log: LoggingAdapter): Commitments = {
val commitments1 = copy(active = active.map {
case c if c.fundingTxId == txid =>
log.info(s"setting localFundingStatus=${status.getClass.getSimpleName} for funding txid=$txid")
c.copy(localFundingStatus = status)
case c => c
}).pruneCommitments()
if (!active.exists(_.fundingTxId == txid)) {
log.error(s"funding txid=$txid doesn't match any of our funding txs")
} else if (commitments1 == this) {
log.warning(s"setting status=${status.getClass.getSimpleName} for funding txid=$txid was a no-op")
def updateLocalFundingStatus(txId: ByteVector32, status: LocalFundingStatus)(implicit log: LoggingAdapter): Either[Commitments, (Commitments, Commitment)] = {
if (!this.active.exists(_.fundingTxId == txId)) {
log.error(s"funding txid=$txId doesn't match any of our funding txs")
Left(this)
} else {
val commitments1 = copy(active = active.map {
case c if c.fundingTxId == txId =>
log.info(s"setting localFundingStatus=${status.getClass.getSimpleName} for funding txid=$txId")
c.copy(localFundingStatus = status)
case c => c
}).pruneCommitments()
val commitment = commitments1.active.find(_.fundingTxId == txId).get
Right(commitments1, commitment)
}
commitments1
}

/**
Expand Down
142 changes: 74 additions & 68 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
import fr.acinq.eclair.channel.Commitments.PostRevocationAction
import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult
import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees, scidForChannelUpdate}
import fr.acinq.eclair.channel.Commitments.PostRevocationAction
import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage
import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.channel._
Expand Down Expand Up @@ -1072,30 +1072,30 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_CLOSING) => handleFundingTimeout(d)

case Event(w: WatchFundingConfirmedTriggered, d: DATA_CLOSING) =>
val commitments1 = d.commitments.updateLocalFundingStatus(w.tx.txid, LocalFundingStatus.ConfirmedFundingTx(w.tx))
if (d.commitments.latest.fundingTxId == w.tx.txid) {
// The best funding tx candidate has been confirmed, alternative commitments have been pruned
stay() using d.copy(commitments = commitments1) storing()
} else {
// This is a corner case where:
// - we are using dual funding
// - *and* the funding tx was RBF-ed
// - *and* we went to CLOSING before any funding tx got confirmed (probably due to a local or remote error)
// - *and* an older version of the funding tx confirmed and reached min depth (it won't be re-orged out)
//
// This means that:
// - the whole current commitment tree has been double-spent and can safely be forgotten
// - from now on, we only need to keep track of the commitment associated to the funding tx that got confirmed
//
// Force-closing is our only option here, if we are in this state the channel was closing and it is too late
// to negotiate a mutual close.
log.info("channelId={} was confirmed at blockHeight={} txIndex={} with a previous funding txid={}", d.channelId, w.blockHeight, w.txIndex, w.tx.txid)
watchFundingSpent(commitments1.latest.commitment)
context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx))
val commitTx = commitments1.latest.fullySignedLocalCommitTx(keyManager).tx
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, commitments1.latest, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf, d.finalScriptPubKey)
val d1 = DATA_CLOSING(commitments1, d.waitingSince, d.finalScriptPubKey, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished))
stay() using d1 storing() calling doPublish(localCommitPublished, commitments1.latest)
acceptFundingTxConfirmed(w, d) match {
case Right((commitments1, _)) =>
if (d.commitments.latest.fundingTxId == w.tx.txid) {
// The best funding tx candidate has been confirmed, alternative commitments have been pruned
stay() using d.copy(commitments = commitments1) storing()
} else {
// This is a corner case where:
// - we are using dual funding
// - *and* the funding tx was RBF-ed
// - *and* we went to CLOSING before any funding tx got confirmed (probably due to a local or remote error)
// - *and* an older version of the funding tx confirmed and reached min depth (it won't be re-orged out)
//
// This means that:
// - the whole current commitment tree has been double-spent and can safely be forgotten
// - from now on, we only need to keep track of the commitment associated to the funding tx that got confirmed
//
// Force-closing is our only option here, if we are in this state the channel was closing and it is too late
// to negotiate a mutual close.
log.info("channelId={} was confirmed at blockHeight={} txIndex={} with a previous funding txid={}", d.channelId, w.blockHeight, w.txIndex, w.tx.txid)
val commitment = commitments1.latest
val d1 = d.copy(commitments = commitments1)
spendLocalCurrent(d1)
}
case Left(_) => stay()
}

case Event(WatchFundingSpentTriggered(tx), d: DATA_CLOSING) =>
Expand Down Expand Up @@ -1563,52 +1563,58 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case Event(TickChannelOpenTimeout, _) => stay()

case Event(w: WatchPublishedTriggered, d: PersistentChannelData) =>
log.info(s"zero-conf funding txid=${w.tx.txid} has been published")
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx)
val commitments1 = d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus)
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
val d1 = d match {
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Unknown
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Unknown
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
case d: DATA_NORMAL => d.copy(commitments = commitments1)
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
case d: DATA_CLOSING => d.copy(commitments = commitments1)
}
stay() using d1 storing()
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
case Right((commitments1, _)) =>
log.info(s"zero-conf funding txid=${w.tx.txid} has been published")
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
val d1 = d match {
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Unknown
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Unknown
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
case d: DATA_NORMAL => d.copy(commitments = commitments1)
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
case d: DATA_CLOSING => d.copy(commitments = commitments1)
}
stay() using d1 storing()
case Left(_) => stay()
}

case Event(w: WatchFundingConfirmedTriggered, d: PersistentChannelData) =>
log.info(s"funding txid=${w.tx.txid} has been confirmed")
val commitments1 = acceptFundingTxConfirmed(w, d)
val d1 = d match {
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitments1.latest.commitInput.outPoint.index.toInt))
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitments1.latest.commitInput.outPoint.index.toInt))
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
case d: DATA_NORMAL => d.copy(commitments = commitments1)
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
case d: DATA_CLOSING => d // there is a dedicated handler in CLOSING state
}
stay() using d1 storing()
acceptFundingTxConfirmed(w, d) match {
case Right((commitments1, commitment)) =>
log.info(s"funding txid=${w.tx.txid} has been confirmed")
val d1 = d match {
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitment.commitInput.outPoint.index.toInt))
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitment.commitInput.outPoint.index.toInt))
val shortIds = createShortIds(d.channelId, realScidStatus)
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
case d: DATA_NORMAL => d.copy(commitments = commitments1)
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
case d: DATA_CLOSING => d // there is a dedicated handler in CLOSING state
}
stay() using d1 storing()
case Left(_) => stay()
}

case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) =>
// they can publish a closing tx with any sig we sent them, even if we are not done negotiating
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,33 +580,38 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
case Event(w: WatchPublishedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
log.info("funding txid={} was successfully published for zero-conf channelId={}", w.tx.txid, d.channelId)
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx)
val commitments1 = d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus)
// we still watch the funding tx for confirmation even if we can use the zero-conf channel right away
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
val realScidStatus = RealScidStatus.Unknown
val shortIds = createShortIds(d.channelId, realScidStatus)
val channelReady = createChannelReady(shortIds, d.commitments.params)
d.deferred.foreach(self ! _)
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
case Right((commitments1, _)) =>
// we still watch the funding tx for confirmation even if we can use the zero-conf channel right away
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
val realScidStatus = RealScidStatus.Unknown
val shortIds = createShortIds(d.channelId, realScidStatus)
val channelReady = createChannelReady(shortIds, d.commitments.params)
d.deferred.foreach(self ! _)
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady
case Left(_) => stay()
}

case Event(w: WatchFundingConfirmedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
log.info("funding txid={} was confirmed at blockHeight={} txIndex={}", w.tx.txid, w.blockHeight, w.txIndex)
val commitments1 = acceptFundingTxConfirmed(w, d)
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, d.commitments.latest.commitInput.outPoint.index.toInt))
val shortIds = createShortIds(d.channelId, realScidStatus)
val channelReady = createChannelReady(shortIds, d.commitments.params)
val toSend = d.rbfStatus match {
case RbfStatus.RbfInProgress(txBuilder) =>
txBuilder ! InteractiveTxBuilder.Abort
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
case RbfStatus.RbfRequested(cmd) =>
cmd.replyTo ! Status.Failure(InvalidRbfTxConfirmed(d.channelId))
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
case RbfStatus.NoRbf | RbfStatus.RbfAborted =>
Seq(channelReady)
acceptFundingTxConfirmed(w, d) match {
case Right((commitments1, commitment)) =>
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitment.commitInput.outPoint.index.toInt))
val shortIds = createShortIds(d.channelId, realScidStatus)
val channelReady = createChannelReady(shortIds, d.commitments.params)
val toSend = d.rbfStatus match {
case RbfStatus.RbfInProgress(txBuilder) =>
txBuilder ! InteractiveTxBuilder.Abort
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
case RbfStatus.RbfRequested(cmd) =>
cmd.replyTo ! Status.Failure(InvalidRbfTxConfirmed(d.channelId))
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
case RbfStatus.NoRbf | RbfStatus.RbfAborted =>
Seq(channelReady)
}
d.deferred.foreach(self ! _)
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending toSend
case Left(_) => stay()
}
d.deferred.foreach(self ! _)
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending toSend

case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleNewBlockDualFundingUnconfirmed(c, d)

Expand Down
Loading

0 comments on commit df590d8

Please sign in to comment.