Skip to content

Commit c45cf33

Browse files
committed
Require strict re-signing before shutdown update
Whenever we exchange `shutdown`, we now require that new closing txs are signed before allowing another `shutdown` message to be sent to start a new signing round. This creates more risk of deadlock when one side fails to send their sigs, where we'll need to disconnect to start a new signing round. But that shouldn't happen if nodes are honest and not buggy, so it probably doesn't matter. If nodes are buggy or malicious, we will need to force-close anyway.
1 parent df6ad3e commit c45cf33

File tree

8 files changed

+111
-122
lines changed

8 files changed

+111
-122
lines changed

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

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -536,36 +536,18 @@ object SpliceStatus {
536536
case object SpliceAborted extends SpliceStatus
537537
}
538538

539-
case class ClosingCompleteSent(closingComplete: ClosingComplete, closingFeerate: FeeratePerKw)
540-
541-
sealed trait OnRemoteShutdown
542-
object OnRemoteShutdown {
543-
/** When receiving the remote shutdown, we sign a new version of our closing transaction. */
544-
case class SignTransaction(closingFeerate: FeeratePerKw) extends OnRemoteShutdown
545-
/** When receiving the remote shutdown, we don't sign a new version of our closing transaction, but our peer may sign theirs. */
546-
case object WaitForSigs extends OnRemoteShutdown
547-
}
548-
549539
sealed trait ClosingNegotiation {
550540
def localShutdown: Shutdown
551-
// When we disconnect, we discard pending signatures.
552-
def disconnect(): ClosingNegotiation.WaitingForRemoteShutdown = this match {
553-
case status: ClosingNegotiation.WaitingForRemoteShutdown => status
554-
case status: ClosingNegotiation.SigningTransactions => status.closingCompleteSent_opt.map(_.closingFeerate) match {
555-
// If we were waiting for their signature, we will send closing_complete again after exchanging shutdown.
556-
case Some(closingFeerate) if status.closingSigReceived_opt.isEmpty => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate))
557-
case _ => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.WaitForSigs)
558-
}
559-
case status: ClosingNegotiation.WaitingForConfirmation => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.WaitForSigs)
560-
}
541+
/** Closing feerate for our closing transaction. */
542+
def closingFeerate: FeeratePerKw
561543
}
562544
object ClosingNegotiation {
563545
/** We've sent a new shutdown message: we wait for their shutdown message before taking any action. */
564-
case class WaitingForRemoteShutdown(localShutdown: Shutdown, onRemoteShutdown: OnRemoteShutdown) extends ClosingNegotiation
565-
/** We've exchanged shutdown messages: at least one side will send closing_complete to renew their closing transaction. */
566-
case class SigningTransactions(localShutdown: Shutdown, remoteShutdown: Shutdown, closingCompleteSent_opt: Option[ClosingCompleteSent], closingSigSent_opt: Option[ClosingSig], closingSigReceived_opt: Option[ClosingSig]) extends ClosingNegotiation
567-
/** We've signed a new closing transaction and are waiting for confirmation or to initiate RBF. */
568-
case class WaitingForConfirmation(localShutdown: Shutdown, remoteShutdown: Shutdown) extends ClosingNegotiation
546+
case class WaitingForRemoteShutdown(localShutdown: Shutdown, closingFeerate: FeeratePerKw) extends ClosingNegotiation
547+
/** We've exchanged shutdown messages: we both send closing_complete to renew the closing transactions. */
548+
case class SigningTransactions(localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerate: FeeratePerKw, closingCompleteSent_opt: Option[ClosingComplete], closingSigSent_opt: Option[ClosingSig], closingSigReceived_opt: Option[ClosingSig]) extends ClosingNegotiation
549+
/** We've signed new closing transactions and are waiting for confirmation or to initiate RBF. */
550+
case class WaitingForConfirmation(localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerate: FeeratePerKw) extends ClosingNegotiation
569551
}
570552

571553
sealed trait ChannelData extends PossiblyHarmful {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ case class InvalidAnnouncementSignatures (override val channelId: Byte
117117
case class InvalidCommitmentSignature (override val channelId: ByteVector32, fundingTxId: TxId, fundingTxIndex: Long, unsignedCommitTx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: fundingTxId=$fundingTxId fundingTxIndex=$fundingTxIndex commitTxId=${unsignedCommitTx.txid} commitTx=$unsignedCommitTx")
118118
case class InvalidHtlcSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid htlc signature: txId=$txId")
119119
case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed")
120+
case class ShutdownWaitingForSigs (override val channelId: ByteVector32) extends ChannelException(channelId, "received unexpected shutdown while signing closing transactions")
120121
case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output")
121122
case class InvalidCloseSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid close signature: txId=$txId")
122123
case class UnexpectedClosingComplete (override val channelId: ByteVector32, fees: Satoshi, lockTime: Long) extends ChannelException(channelId, s"unexpected closing_complete with fees=$fees and lockTime=$lockTime: we already sent closing_sig, you must send shutdown first")

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

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,77 +1727,69 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
17271727

17281728
when(NEGOTIATING_SIMPLE)(handleExceptions {
17291729
case Event(remoteShutdown: Shutdown, d: DATA_NEGOTIATING_SIMPLE) =>
1730+
val localScript = d.status.localShutdown.scriptPubKey
1731+
val remoteScript = remoteShutdown.scriptPubKey
17301732
d.status match {
17311733
case status: ClosingNegotiation.WaitingForRemoteShutdown =>
17321734
// We have already sent our shutdown. Now that we've received theirs, we're ready to sign closing transactions.
1733-
// If we don't have a closing feerate, we don't need to create a new version of our closing transaction (which
1734-
// can happen after a reconnection for example).
1735-
status.onRemoteShutdown match {
1736-
case OnRemoteShutdown.SignTransaction(closingFeerate) =>
1737-
val localScript = status.localShutdown.scriptPubKey
1738-
val remoteScript = remoteShutdown.scriptPubKey
1739-
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, remoteScript, closingFeerate) match {
1740-
case Left(f) =>
1741-
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
1742-
val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, None, None, None)
1743-
stay() using d.copy(status = status1)
1744-
case Right((closingTxs, closingComplete)) =>
1745-
log.debug("signing local mutual close transactions: {}", closingTxs)
1746-
val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, Some(ClosingCompleteSent(closingComplete, closingFeerate)), None, None)
1747-
stay() using d.copy(status = status1, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending closingComplete
1748-
}
1749-
case OnRemoteShutdown.WaitForSigs =>
1750-
val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, None, None, None)
1735+
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, remoteScript, status.closingFeerate) match {
1736+
case Left(f) =>
1737+
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
1738+
val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, status.closingFeerate, None, None, None)
17511739
stay() using d.copy(status = status1)
1740+
case Right((closingTxs, closingComplete)) =>
1741+
log.debug("signing local mutual close transactions: {}", closingTxs)
1742+
val status1 = ClosingNegotiation.SigningTransactions(status.localShutdown, remoteShutdown, status.closingFeerate, Some(closingComplete), None, None)
1743+
stay() using d.copy(status = status1, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending closingComplete
17521744
}
1753-
case status: ClosingNegotiation.SigningTransactions =>
1754-
// We were in the middle of signing transactions: we restart a signing round from scratch.
1755-
// If we were waiting for their signature, we will send closing_complete again after exchanging shutdown.
1756-
val localShutdown = status.localShutdown
1757-
val onRemoteShutdown = status.closingCompleteSent_opt.map(_.closingFeerate) match {
1758-
case Some(closingFeerate) if status.closingSigReceived_opt.isEmpty => OnRemoteShutdown.SignTransaction(closingFeerate)
1759-
case _ => OnRemoteShutdown.WaitForSigs
1760-
}
1761-
val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, onRemoteShutdown)
1762-
self ! remoteShutdown
1763-
stay() using d.copy(status = status1) sending localShutdown
1745+
case _: ClosingNegotiation.SigningTransactions =>
1746+
// We were in the middle of signing transactions: sending shutdown is forbidden at that point.
1747+
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
1748+
stay() sending Warning(d.channelId, ShutdownWaitingForSigs(d.channelId).getMessage)
17641749
case status: ClosingNegotiation.WaitingForConfirmation =>
17651750
// Our peer wants to create a new version of their closing transaction. We don't need to update our version of
1766-
// the closing transaction: we re-send our previous shutdown and wait for their closing_complete.
1751+
// the closing transaction: we use the same parameters as we did in the previous signing round.
17671752
val localShutdown = status.localShutdown
1768-
val status1 = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None)
1769-
stay() using d.copy(status = status1) sending localShutdown
1753+
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, remoteScript, status.closingFeerate) match {
1754+
case Left(f) =>
1755+
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
1756+
val status1 = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, status.closingFeerate, None, None, None)
1757+
stay() using d.copy(status = status1) sending localShutdown
1758+
case Right((closingTxs, closingComplete)) =>
1759+
log.debug("signing local mutual close transactions: {}", closingTxs)
1760+
val status1 = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, status.closingFeerate, Some(closingComplete), None, None)
1761+
stay() using d.copy(status = status1, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending Seq(localShutdown, closingComplete)
1762+
}
17701763
}
17711764

17721765
case Event(closingComplete: ClosingComplete, d: DATA_NEGOTIATING_SIMPLE) =>
17731766
d.status match {
17741767
case _: ClosingNegotiation.WaitingForRemoteShutdown =>
17751768
log.info("ignoring remote closing_complete, we've sent shutdown to initiate a new signing round")
1776-
stay()
1769+
stay() sending Warning(d.channelId, UnexpectedClosingComplete(d.channelId, closingComplete.fees, closingComplete.lockTime).getMessage)
17771770
case _: ClosingNegotiation.WaitingForConfirmation =>
17781771
log.info("ignoring closing_complete, we've already sent closing_sig: peer must send shutdown again before closing_complete")
17791772
stay() sending Warning(d.channelId, UnexpectedClosingComplete(d.channelId, closingComplete.fees, closingComplete.lockTime).getMessage)
17801773
case status: ClosingNegotiation.SigningTransactions if status.closingSigSent_opt.nonEmpty =>
1781-
log.info("ignoring closing_complete, we've already sent closing_sig: peer must send shutdown again before closing_complete")
1774+
log.info("ignoring closing_complete, we've already sent closing_sig: peer must send closing_sig, then shutdown again before closing_complete")
17821775
stay() sending Warning(d.channelId, UnexpectedClosingComplete(d.channelId, closingComplete.fees, closingComplete.lockTime).getMessage)
17831776
case status: ClosingNegotiation.SigningTransactions =>
17841777
val localScript = status.localShutdown.scriptPubKey
17851778
val remoteScript = status.remoteShutdown.scriptPubKey
17861779
MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, localScript, remoteScript, closingComplete) match {
17871780
case Left(f) =>
1788-
// This may happen if scripts were updated concurrently, so we simply ignore failures.
17891781
log.warning("invalid closing_complete: {}", f.getMessage)
1790-
stay()
1782+
stay() sending Warning(d.channelId, f.getMessage)
17911783
case Right((signedClosingTx, closingSig)) =>
17921784
log.debug("signing remote mutual close transaction: {}", signedClosingTx.tx)
17931785
val status1 = status.closingCompleteSent_opt match {
17941786
// We've sent closing_complete: we may be waiting for their closing_sig.
17951787
case Some(_) => status.closingSigReceived_opt match {
1796-
case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown)
1788+
case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown, status.closingFeerate)
17971789
case None => status.copy(closingSigSent_opt = Some(closingSig))
17981790
}
1799-
// We haven't sent closing_complete: we're not waiting for their closing_sig'.
1800-
case None => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown)
1791+
// We haven't sent closing_complete: we're not waiting for their closing_sig.
1792+
case None => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown, status.closingFeerate)
18011793
}
18021794
val d1 = d.copy(status = status1, publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx)
18031795
stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = false) sending closingSig
@@ -1818,14 +1810,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
18181810
case status: ClosingNegotiation.SigningTransactions =>
18191811
MutualClose.receiveSimpleClosingSig(keyManager, d.commitments.latest, d.proposedClosingTxs.last, closingSig) match {
18201812
case Left(f) =>
1821-
// This may happen if scripts were updated concurrently, so we simply ignore failures.
18221813
log.warning("invalid closing_sig: {}", f.getMessage)
1823-
stay()
1814+
stay() sending Warning(d.channelId, f.getMessage)
18241815
case Right(signedClosingTx) =>
18251816
log.debug("received signatures for local mutual close transaction: {}", signedClosingTx.tx)
18261817
val status1 = status.closingSigSent_opt match {
18271818
// We have already signed their transaction: both local and remote closing transactions have been updated.
1828-
case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown)
1819+
case Some(_) => ClosingNegotiation.WaitingForConfirmation(status.localShutdown, status.remoteShutdown, status.closingFeerate)
18291820
// We haven't sent closing_sig yet: they may send us closing_complete to update their closing transaction.
18301821
case None => status.copy(closingSigReceived_opt = Some(closingSig))
18311822
}
@@ -1859,17 +1850,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
18591850
log.info("we're already waiting for our peer to send their shutdown message, no need to send ours again")
18601851
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
18611852
case _: ClosingNegotiation.SigningTransactions =>
1862-
val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate))
1863-
stay() using d.copy(status = status1) storing() sending localShutdown
1853+
log.info("we're in the middle of signing closing transactions, we should finish this round before starting a new signing session")
1854+
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
18641855
case _: ClosingNegotiation.WaitingForConfirmation =>
1865-
val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate))
1856+
val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, closingFeerate)
18661857
stay() using d.copy(status = status1) storing() sending localShutdown
18671858
}
18681859

18691860
case Event(e: Error, d: DATA_NEGOTIATING_SIMPLE) => handleRemoteError(e, d)
18701861

18711862
case Event(INPUT_DISCONNECTED, d: DATA_NEGOTIATING_SIMPLE) =>
1872-
val status1 = d.status.disconnect()
1863+
val status1 = ClosingNegotiation.WaitingForRemoteShutdown(d.status.localShutdown, d.status.closingFeerate)
18731864
goto(OFFLINE) using d.copy(status = status1)
18741865

18751866
})
@@ -2526,10 +2517,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
25262517
case Event(_: ChannelReestablish, d: DATA_NEGOTIATING_SIMPLE) =>
25272518
// We retransmit our shutdown: we may have updated our script and they may not have received it.
25282519
val localShutdown = d.status.localShutdown
2529-
val status1 = d.status match {
2530-
case status: ClosingNegotiation.WaitingForRemoteShutdown => status.copy(localShutdown = localShutdown)
2531-
case _ => ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, OnRemoteShutdown.WaitForSigs)
2532-
}
2520+
val status1 = ClosingNegotiation.WaitingForRemoteShutdown(localShutdown, d.status.closingFeerate)
25332521
goto(NEGOTIATING_SIMPLE) using d.copy(status = status1) sending localShutdown
25342522

25352523
// This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ trait CommonHandlers {
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)
142-
val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None)
142+
val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, None, None, None)
143143
val d = DATA_NEGOTIATING_SIMPLE(commitments, status, Nil, Nil)
144144
goto(NEGOTIATING_SIMPLE) using d storing() sending toSend
145145
case Right((closingTxs, closingComplete)) =>
146146
log.debug("signing local mutual close transactions: {}", closingTxs)
147-
val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, Some(ClosingCompleteSent(closingComplete, closingFeerate)), None, None)
147+
val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, closingFeerate, Some(closingComplete), None, None)
148148
val d = DATA_NEGOTIATING_SIMPLE(commitments, status, closingTxs :: Nil, Nil)
149149
goto(NEGOTIATING_SIMPLE) using d storing() sending toSend :+ closingComplete
150150
}

0 commit comments

Comments
 (0)