Skip to content

Commit 9210dff

Browse files
authored
Keep track on which side initiated a mutual close (#3042)
We currently lose this information once we transition to `SHUTDOWN` (in `NORMAL`, we can compute it based on which `shutdown` is present). This change introduces a proper `CloseStatus`, optional in `NORMAL` state, mandatory in `SHUTDOWN` state.
1 parent 9a63ed9 commit 9210dff

File tree

13 files changed

+105
-51
lines changed

13 files changed

+105
-51
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,14 @@ object DualFundingStatus {
481481
case object RbfAborted extends DualFundingStatus
482482
}
483483

484+
sealed trait CloseStatus {
485+
def feerates_opt: Option[ClosingFeerates]
486+
}
487+
object CloseStatus {
488+
final case class Initiator(override val feerates_opt: Option[ClosingFeerates]) extends CloseStatus
489+
final case class NonInitiator(override val feerates_opt: Option[ClosingFeerates]) extends CloseStatus
490+
}
491+
484492
/** We're waiting for the channel to be quiescent. */
485493
sealed trait QuiescenceNegotiation
486494
object QuiescenceNegotiation {
@@ -629,14 +637,14 @@ final case class DATA_NORMAL(commitments: Commitments,
629637
channelUpdate: ChannelUpdate,
630638
localShutdown: Option[Shutdown],
631639
remoteShutdown: Option[Shutdown],
632-
closingFeerates: Option[ClosingFeerates],
640+
closeStatus_opt: Option[CloseStatus],
633641
spliceStatus: SpliceStatus) extends ChannelDataWithCommitments {
634642
val lastAnnouncedCommitment_opt: Option[AnnouncedCommitment] = lastAnnouncement_opt.flatMap(ann => commitments.resolveCommitment(ann.shortChannelId).map(c => AnnouncedCommitment(c, ann)))
635643
val lastAnnouncedFundingTxId_opt: Option[TxId] = lastAnnouncedCommitment_opt.map(_.fundingTxId)
636644
val isNegotiatingQuiescence: Boolean = spliceStatus.isNegotiatingQuiescence
637645
val isQuiescent: Boolean = spliceStatus.isQuiescent
638646
}
639-
final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends ChannelDataWithCommitments
647+
final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus) extends ChannelDataWithCommitments
640648
final case class DATA_NEGOTIATING(commitments: Commitments,
641649
localShutdown: Shutdown, remoteShutdown: Shutdown,
642650
closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection)

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

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -686,9 +686,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
686686
// we were waiting for our pending htlcs to be signed before replying with our local shutdown
687687
val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d)
688688
val localShutdown = Shutdown(d.channelId, finalScriptPubKey)
689+
// this should always be defined, we provide a fallback for backward compat with older channels
690+
val closeStatus = d.closeStatus_opt.getOrElse(CloseStatus.NonInitiator(None))
689691
// note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING
690692
require(commitments1.latest.remoteCommit.spec.htlcs.nonEmpty, "we must have just signed new htlcs, otherwise we would have sent our Shutdown earlier")
691-
goto(SHUTDOWN) using DATA_SHUTDOWN(commitments1, localShutdown, d.remoteShutdown.get, d.closingFeerates) storing() sending localShutdown
693+
goto(SHUTDOWN) using DATA_SHUTDOWN(commitments1, localShutdown, d.remoteShutdown.get, closeStatus) storing() sending localShutdown
692694
} else {
693695
stay() using d.copy(commitments = commitments1) storing()
694696
}
@@ -711,7 +713,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
711713
case Left(e) => handleCommandError(e, c)
712714
case Right(localShutdownScript) =>
713715
val shutdown = Shutdown(d.channelId, localShutdownScript)
714-
handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closingFeerates = c.feerates)) storing() sending shutdown
716+
handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closeStatus_opt = Some(CloseStatus.Initiator(c.feerates)))) storing() sending shutdown
715717
}
716718
}
717719

@@ -752,7 +754,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
752754
self ! CMD_SIGN()
753755
}
754756
// in the meantime we won't send new changes
755-
stay() using d.copy(remoteShutdown = Some(remoteShutdown))
757+
stay() using d.copy(remoteShutdown = Some(remoteShutdown), closeStatus_opt = Some(CloseStatus.NonInitiator(None)))
756758
} else {
757759
// so we don't have any unsigned outgoing changes
758760
val (localShutdown, sendList) = d.localShutdown match {
@@ -763,23 +765,29 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
763765
// we need to send our shutdown if we didn't previously
764766
(localShutdown, localShutdown :: Nil)
765767
}
768+
val closeStatus = d.localShutdown match {
769+
case Some(_) =>
770+
// this should always be defined, we provide a fallback for backward compat with older channels
771+
d.closeStatus_opt.getOrElse(CloseStatus.Initiator(None))
772+
case None => CloseStatus.NonInitiator(None)
773+
}
766774
// are there pending signed changes on either side? we need to have received their last revocation!
767775
if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) {
768776
// there are no pending signed changes, let's directly negotiate a closing transaction
769777
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
770-
val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates)
778+
val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus)
771779
goto(NEGOTIATING_SIMPLE) using d1 storing() sending sendList ++ closingComplete_opt.toSeq
772780
} else if (d.commitments.params.localParams.paysClosingFees) {
773781
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
774-
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closingFeerates)
782+
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closeStatus.feerates_opt)
775783
goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned
776784
} else {
777785
// we are not the channel initiator, will wait for their closing_signed
778786
goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending sendList
779787
}
780788
} else {
781789
// there are some pending signed changes, we need to wait for them to be settled (fail/fulfill htlcs and sign fee updates)
782-
goto(SHUTDOWN) using DATA_SHUTDOWN(d.commitments, localShutdown, remoteShutdown, d.closingFeerates) storing() sending sendList
790+
goto(SHUTDOWN) using DATA_SHUTDOWN(d.commitments, localShutdown, remoteShutdown, closeStatus) storing() sending sendList
783791
}
784792
}
785793
}
@@ -1569,7 +1577,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
15691577
stay()
15701578
}
15711579

1572-
case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closingFeerates)) =>
1580+
case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closeStatus)) =>
15731581
aggregateSigs(commit) match {
15741582
case Some(sigs) =>
15751583
d.commitments.receiveCommit(sigs, keyManager) match {
@@ -1579,11 +1587,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
15791587
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
15801588
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
15811589
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
1582-
val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates)
1590+
val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus)
15831591
goto(NEGOTIATING_SIMPLE) using d1 storing() sending revocation +: closingComplete_opt.toSeq
15841592
} else if (d.commitments.params.localParams.paysClosingFees) {
15851593
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
1586-
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closingFeerates)
1594+
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt)
15871595
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil
15881596
} else {
15891597
// we are not the channel initiator, will wait for their closing_signed
@@ -1601,7 +1609,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
16011609
case None => stay()
16021610
}
16031611

1604-
case Event(revocation: RevokeAndAck, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closingFeerates)) =>
1612+
case Event(revocation: RevokeAndAck, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closeStatus)) =>
16051613
// we received a revocation because we sent a signature
16061614
// => all our changes have been acked including the shutdown message
16071615
d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match {
@@ -1624,11 +1632,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
16241632
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
16251633
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
16261634
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
1627-
val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates)
1635+
val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closeStatus)
16281636
goto(NEGOTIATING_SIMPLE) using d1 storing() sending closingComplete_opt.toSeq
16291637
} else if (d.commitments.params.localParams.paysClosingFees) {
16301638
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
1631-
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closingFeerates)
1639+
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt)
16321640
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned
16331641
} else {
16341642
// we are not the channel initiator, will wait for their closing_signed
@@ -1664,7 +1672,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
16641672
if (c.scriptPubKey.exists(_ != d.localShutdown.scriptPubKey) && !useSimpleClose) {
16651673
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
16661674
} else if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) {
1667-
val d1 = d.copy(localShutdown = localShutdown_opt.getOrElse(d.localShutdown), closingFeerates = c.feerates.orElse(d.closingFeerates))
1675+
val closeStatus1 = d.closeStatus match {
1676+
case initiator: CloseStatus.Initiator => initiator.copy(feerates_opt = c.feerates.orElse(initiator.feerates_opt))
1677+
case nonInitiator: CloseStatus.NonInitiator => nonInitiator.copy(feerates_opt = c.feerates.orElse(nonInitiator.feerates_opt)) // NB: this is the corner case where we can be non-initiator and have custom feerates
1678+
}
1679+
val d1 = d.copy(localShutdown = localShutdown_opt.getOrElse(d.localShutdown), closeStatus = closeStatus1)
16681680
handleCommandSuccess(c, d1) storing() sending localShutdown_opt.toSeq
16691681
} else {
16701682
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.fsm
1818

1919
import akka.actor.FSM
2020
import fr.acinq.bitcoin.scalacompat.ByteVector32
21-
import fr.acinq.eclair.Features
21+
import fr.acinq.eclair.{Features, MilliSatoshiLong}
2222
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
2323
import fr.acinq.eclair.channel._
2424
import fr.acinq.eclair.db.PendingCommandsDb
@@ -132,10 +132,10 @@ trait CommonHandlers {
132132
finalScriptPubkey
133133
}
134134

135-
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = {
135+
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = {
136136
val localScript = localShutdown.scriptPubKey
137137
val remoteScript = remoteShutdown.scriptPubKey
138-
val closingFeerate = closingFeerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
138+
val closingFeerate = closeStatus.feerates_opt.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
139139
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match {
140140
case Left(f) =>
141141
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ private[channel] object ChannelCodecs0 {
404404
("channelUpdate" | variableSizeBytes(noUnknownFieldsChannelUpdateSizeCodec, channelUpdateCodec)) ::
405405
("localShutdown" | optional(bool, shutdownCodec)) ::
406406
("remoteShutdown" | optional(bool, shutdownCodec)) ::
407-
("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map {
407+
("closeStatus" | provide(Option.empty[CloseStatus]))).map {
408408
case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil =>
409409
val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)
410410
DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice)
@@ -418,7 +418,7 @@ private[channel] object ChannelCodecs0 {
418418
("channelUpdate" | variableSizeBytes(uint16, channelUpdateCodec)) ::
419419
("localShutdown" | optional(bool, shutdownCodec)) ::
420420
("remoteShutdown" | optional(bool, shutdownCodec)) ::
421-
("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map {
421+
("closeStatus" | provide(Option.empty[CloseStatus]))).map {
422422
case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil =>
423423
val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)
424424
DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice)
@@ -428,7 +428,7 @@ private[channel] object ChannelCodecs0 {
428428
("commitments" | commitmentsCodec) ::
429429
("localShutdown" | shutdownCodec) ::
430430
("remoteShutdown" | shutdownCodec) ::
431-
("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN].decodeOnly
431+
("closeStatus" | provide[CloseStatus](CloseStatus.Initiator(None)))).as[DATA_SHUTDOWN].decodeOnly
432432

433433
val DATA_NEGOTIATING_05_Codec: Codec[DATA_NEGOTIATING] = (
434434
("commitments" | commitmentsCodec) ::

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,17 +261,17 @@ private[channel] object ChannelCodecs1 {
261261
("channelUpdate" | lengthDelimited(channelUpdateCodec)) ::
262262
("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) ::
263263
("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) ::
264-
("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map {
265-
case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil =>
264+
("closeStatus" | provide(Option.empty[CloseStatus]))).map {
265+
case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closeStatus :: HNil =>
266266
val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)
267-
DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice)
267+
DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus, SpliceStatus.NoSplice)
268268
}.decodeOnly
269269

270270
val DATA_SHUTDOWN_23_Codec: Codec[DATA_SHUTDOWN] = (
271271
("commitments" | commitmentsCodec) ::
272272
("localShutdown" | lengthDelimited(shutdownCodec)) ::
273273
("remoteShutdown" | lengthDelimited(shutdownCodec)) ::
274-
("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN]
274+
("closeStatus" | provide[CloseStatus](CloseStatus.Initiator(None)))).as[DATA_SHUTDOWN]
275275

276276
val DATA_NEGOTIATING_24_Codec: Codec[DATA_NEGOTIATING] = (
277277
("commitments" | commitmentsCodec) ::

0 commit comments

Comments
 (0)