Skip to content

Commit 36ff0c3

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 623b371 commit 36ff0c3

File tree

9 files changed

+361
-107
lines changed

9 files changed

+361
-107
lines changed

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

Lines changed: 27 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, FailureMessage, 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, FailureMessage, 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,30 @@ object SpliceStatus {
536536
case object SpliceAborted extends SpliceStatus
537537
}
538538

539+
case class ClosingCompleteSent(closingComplete: ClosingComplete, closingFeerate: FeeratePerKw)
540+
541+
sealed trait ClosingNegotiation {
542+
def localShutdown: Shutdown
543+
// When we disconnect, we discard pending signatures.
544+
def disconnect(): ClosingNegotiation.WaitingForRemoteShutdown = this match {
545+
case status: ClosingNegotiation.WaitingForRemoteShutdown => status
546+
case status: ClosingNegotiation.SigningTransactions => status.closingCompleteSent_opt.map(_.closingFeerate) match {
547+
// If we were waiting for their signature, we will send closing_complete again after exchanging shutdown.
548+
case Some(closingFeerate) if status.closingSigReceived_opt.isEmpty => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, Some(closingFeerate))
549+
case _ => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, None)
550+
}
551+
case status: ClosingNegotiation.WaitingForConfirmation => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, None)
552+
}
553+
}
554+
object ClosingNegotiation {
555+
/** We've sent a new shutdown message: we wait for their shutdown message before sending closing_complete (if [[sendClosingFeerate_opt]] is provided). */
556+
case class WaitingForRemoteShutdown(localShutdown: Shutdown, sendClosingFeerate_opt: Option[FeeratePerKw]) extends ClosingNegotiation
557+
/** We've exchanged shutdown messages: at least one side will send closing_complete to renew their closing transaction. */
558+
case class SigningTransactions(localShutdown: Shutdown, remoteShutdown: Shutdown, closingCompleteSent_opt: Option[ClosingCompleteSent], closingSigSent_opt: Option[ClosingSig], closingSigReceived_opt: Option[ClosingSig]) extends ClosingNegotiation
559+
/** We've signed a new closing transaction and are waiting for confirmation or to initiate RBF. */
560+
case class WaitingForConfirmation(localShutdown: Shutdown, remoteShutdown: Shutdown) extends ClosingNegotiation
561+
}
562+
539563
sealed trait ChannelData extends PossiblyHarmful {
540564
def channelId: ByteVector32
541565
}
@@ -655,12 +679,13 @@ final case class DATA_NEGOTIATING(commitments: Commitments,
655679
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")
656680
}
657681
final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments,
658-
localShutdown: Shutdown, remoteShutdown: Shutdown,
682+
status: ClosingNegotiation,
659683
// Closing transactions we created, where we pay the fees (unsigned).
660684
proposedClosingTxs: List[ClosingTxs],
661685
// Closing transactions we published: this contains our local transactions for
662686
// which they sent a signature, and their closing transactions that we signed.
663687
publishedClosingTxs: List[ClosingTx]) extends ChannelDataWithCommitments {
688+
val localScriptPubKey: ByteVector = status.localShutdown.scriptPubKey
664689
def findClosingTx(tx: Transaction): Option[ClosingTx] = publishedClosingTxs.find(_.tx.txid == tx.txid).orElse(proposedClosingTxs.flatMap(_.all).find(_.tx.txid == tx.txid))
665690
}
666691
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
@@ -118,6 +118,7 @@ case class InvalidHtlcSignature (override val channelId: Byte
118118
case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed")
119119
case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output")
120120
case class InvalidCloseSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid close signature: txId=$txId")
121+
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")
121122
case class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid closing tx: some outputs are below dust: txId=$txId")
122123
case class CommitSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"commit sig count mismatch: expected=$expected actual=$actual")
123124
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)