Skip to content

Commit df590d8

Browse files
authored
Make updateLocalFundingStatus method return Either (#2602)
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.
1 parent a3c6029 commit df590d8

File tree

5 files changed

+150
-132
lines changed

5 files changed

+150
-132
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,19 +1001,20 @@ case class Commitments(params: ChannelParams,
10011001
active.forall(_.commitInput.redeemScript == fundingScript)
10021002
}
10031003

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

10191020
/**

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 74 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ import fr.acinq.eclair.blockchain._
2929
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher
3030
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
3131
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
32+
import fr.acinq.eclair.channel.Commitments.PostRevocationAction
3233
import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult
3334
import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees, scidForChannelUpdate}
34-
import fr.acinq.eclair.channel.Commitments.PostRevocationAction
3535
import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage
3636
import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
3737
import fr.acinq.eclair.channel._
@@ -1072,30 +1072,30 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
10721072
case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_CLOSING) => handleFundingTimeout(d)
10731073

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

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

15651565
case Event(w: WatchPublishedTriggered, d: PersistentChannelData) =>
1566-
log.info(s"zero-conf funding txid=${w.tx.txid} has been published")
15671566
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx)
1568-
val commitments1 = d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus)
1569-
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
1570-
val d1 = d match {
1571-
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
1572-
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
1573-
val realScidStatus = RealScidStatus.Unknown
1574-
val shortIds = createShortIds(d.channelId, realScidStatus)
1575-
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
1576-
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
1577-
val realScidStatus = RealScidStatus.Unknown
1578-
val shortIds = createShortIds(d.channelId, realScidStatus)
1579-
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
1580-
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
1581-
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
1582-
case d: DATA_NORMAL => d.copy(commitments = commitments1)
1583-
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
1584-
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
1585-
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
1586-
case d: DATA_CLOSING => d.copy(commitments = commitments1)
1587-
}
1588-
stay() using d1 storing()
1567+
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
1568+
case Right((commitments1, _)) =>
1569+
log.info(s"zero-conf funding txid=${w.tx.txid} has been published")
1570+
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
1571+
val d1 = d match {
1572+
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
1573+
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
1574+
val realScidStatus = RealScidStatus.Unknown
1575+
val shortIds = createShortIds(d.channelId, realScidStatus)
1576+
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
1577+
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
1578+
val realScidStatus = RealScidStatus.Unknown
1579+
val shortIds = createShortIds(d.channelId, realScidStatus)
1580+
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
1581+
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
1582+
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
1583+
case d: DATA_NORMAL => d.copy(commitments = commitments1)
1584+
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
1585+
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
1586+
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
1587+
case d: DATA_CLOSING => d.copy(commitments = commitments1)
1588+
}
1589+
stay() using d1 storing()
1590+
case Left(_) => stay()
1591+
}
15891592

15901593
case Event(w: WatchFundingConfirmedTriggered, d: PersistentChannelData) =>
1591-
log.info(s"funding txid=${w.tx.txid} has been confirmed")
1592-
val commitments1 = acceptFundingTxConfirmed(w, d)
1593-
val d1 = d match {
1594-
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
1595-
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
1596-
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitments1.latest.commitInput.outPoint.index.toInt))
1597-
val shortIds = createShortIds(d.channelId, realScidStatus)
1598-
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
1599-
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
1600-
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitments1.latest.commitInput.outPoint.index.toInt))
1601-
val shortIds = createShortIds(d.channelId, realScidStatus)
1602-
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
1603-
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
1604-
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
1605-
case d: DATA_NORMAL => d.copy(commitments = commitments1)
1606-
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
1607-
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
1608-
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
1609-
case d: DATA_CLOSING => d // there is a dedicated handler in CLOSING state
1610-
}
1611-
stay() using d1 storing()
1594+
acceptFundingTxConfirmed(w, d) match {
1595+
case Right((commitments1, commitment)) =>
1596+
log.info(s"funding txid=${w.tx.txid} has been confirmed")
1597+
val d1 = d match {
1598+
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
1599+
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
1600+
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitment.commitInput.outPoint.index.toInt))
1601+
val shortIds = createShortIds(d.channelId, realScidStatus)
1602+
DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds)
1603+
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED =>
1604+
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitment.commitInput.outPoint.index.toInt))
1605+
val shortIds = createShortIds(d.channelId, realScidStatus)
1606+
DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds)
1607+
case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1)
1608+
case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => d.copy(commitments = commitments1)
1609+
case d: DATA_NORMAL => d.copy(commitments = commitments1)
1610+
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
1611+
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
1612+
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
1613+
case d: DATA_CLOSING => d // there is a dedicated handler in CLOSING state
1614+
}
1615+
stay() using d1 storing()
1616+
case Left(_) => stay()
1617+
}
16121618

16131619
case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) =>
16141620
// they can publish a closing tx with any sig we sent them, even if we are not done negotiating

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -580,33 +580,38 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
580580
case Event(w: WatchPublishedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
581581
log.info("funding txid={} was successfully published for zero-conf channelId={}", w.tx.txid, d.channelId)
582582
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx)
583-
val commitments1 = d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus)
584-
// we still watch the funding tx for confirmation even if we can use the zero-conf channel right away
585-
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
586-
val realScidStatus = RealScidStatus.Unknown
587-
val shortIds = createShortIds(d.channelId, realScidStatus)
588-
val channelReady = createChannelReady(shortIds, d.commitments.params)
589-
d.deferred.foreach(self ! _)
590-
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady
583+
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
584+
case Right((commitments1, _)) =>
585+
// we still watch the funding tx for confirmation even if we can use the zero-conf channel right away
586+
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks))
587+
val realScidStatus = RealScidStatus.Unknown
588+
val shortIds = createShortIds(d.channelId, realScidStatus)
589+
val channelReady = createChannelReady(shortIds, d.commitments.params)
590+
d.deferred.foreach(self ! _)
591+
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady
592+
case Left(_) => stay()
593+
}
591594

592595
case Event(w: WatchFundingConfirmedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
593-
log.info("funding txid={} was confirmed at blockHeight={} txIndex={}", w.tx.txid, w.blockHeight, w.txIndex)
594-
val commitments1 = acceptFundingTxConfirmed(w, d)
595-
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, d.commitments.latest.commitInput.outPoint.index.toInt))
596-
val shortIds = createShortIds(d.channelId, realScidStatus)
597-
val channelReady = createChannelReady(shortIds, d.commitments.params)
598-
val toSend = d.rbfStatus match {
599-
case RbfStatus.RbfInProgress(txBuilder) =>
600-
txBuilder ! InteractiveTxBuilder.Abort
601-
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
602-
case RbfStatus.RbfRequested(cmd) =>
603-
cmd.replyTo ! Status.Failure(InvalidRbfTxConfirmed(d.channelId))
604-
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
605-
case RbfStatus.NoRbf | RbfStatus.RbfAborted =>
606-
Seq(channelReady)
596+
acceptFundingTxConfirmed(w, d) match {
597+
case Right((commitments1, commitment)) =>
598+
val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(w.blockHeight, w.txIndex, commitment.commitInput.outPoint.index.toInt))
599+
val shortIds = createShortIds(d.channelId, realScidStatus)
600+
val channelReady = createChannelReady(shortIds, d.commitments.params)
601+
val toSend = d.rbfStatus match {
602+
case RbfStatus.RbfInProgress(txBuilder) =>
603+
txBuilder ! InteractiveTxBuilder.Abort
604+
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
605+
case RbfStatus.RbfRequested(cmd) =>
606+
cmd.replyTo ! Status.Failure(InvalidRbfTxConfirmed(d.channelId))
607+
Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady)
608+
case RbfStatus.NoRbf | RbfStatus.RbfAborted =>
609+
Seq(channelReady)
610+
}
611+
d.deferred.foreach(self ! _)
612+
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending toSend
613+
case Left(_) => stay()
607614
}
608-
d.deferred.foreach(self ! _)
609-
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending toSend
610615

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

0 commit comments

Comments
 (0)