Skip to content

Commit ea979aa

Browse files
committed
Require strict exchange of shutdown
Whenever one side sends `shutdown`, we restart a signing round from scratch. To be compatible with future taproot channels, we require the receiver to also send `shutdown` before moving on to exchanging `closing_complete` and `closing_sig`. This will give nodes a message to exchange fresh musig2 nonces before producing signatures. On reconnection, we also restart a signing session from scratch and discard pending partial signatures.
1 parent b625aba commit ea979aa

File tree

9 files changed

+373
-107
lines changed

9 files changed

+373
-107
lines changed

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS
2626
import fr.acinq.eclair.io.Peer
2727
import fr.acinq.eclair.transactions.CommitmentSpec
2828
import fr.acinq.eclair.transactions.Transactions._
29-
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureReason, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxInitRbf, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
29+
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, CommitSig, FailureReason, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxInitRbf, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
3030
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
3131
import scodec.bits.ByteVector
3232

@@ -536,6 +536,38 @@ 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+
549+
sealed trait ClosingNegotiation {
550+
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+
}
561+
}
562+
object ClosingNegotiation {
563+
/** 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
569+
}
570+
539571
sealed trait ChannelData extends PossiblyHarmful {
540572
def channelId: ByteVector32
541573
}
@@ -655,12 +687,13 @@ final case class DATA_NEGOTIATING(commitments: Commitments,
655687
require(!commitments.params.localParams.paysClosingFees || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing")
656688
}
657689
final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments,
658-
localShutdown: Shutdown, remoteShutdown: Shutdown,
690+
status: ClosingNegotiation,
659691
// Closing transactions we created, where we pay the fees (unsigned).
660692
proposedClosingTxs: List[ClosingTxs],
661693
// Closing transactions we published: this contains our local transactions for
662694
// which they sent a signature, and their closing transactions that we signed.
663695
publishedClosingTxs: List[ClosingTx]) extends ChannelDataWithCommitments {
696+
val localScriptPubKey: ByteVector = status.localShutdown.scriptPubKey
664697
def findClosingTx(tx: Transaction): Option[ClosingTx] = publishedClosingTxs.find(_.tx.txid == tx.txid).orElse(proposedClosingTxs.flatMap(_.all).find(_.tx.txid == tx.txid))
665698
}
666699
final case class DATA_CLOSING(commitments: Commitments,

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
@@ -119,6 +119,7 @@ case class InvalidHtlcSignature (override val channelId: Byte
119119
case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed")
120120
case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output")
121121
case class InvalidCloseSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid close signature: txId=$txId")
122+
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")
122123
case class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid closing tx: some outputs are below dust: txId=$txId")
123124
case class CommitSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"commit sig count mismatch: expected=$expected actual=$actual")
124125
case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual=$actual")

0 commit comments

Comments
 (0)