Skip to content

Commit

Permalink
Require strict exchange of shutdown
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Dec 6, 2024
1 parent b625aba commit ea979aa
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
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}
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}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
import scodec.bits.ByteVector

Expand Down Expand Up @@ -536,6 +536,38 @@ object SpliceStatus {
case object SpliceAborted extends SpliceStatus
}

case class ClosingCompleteSent(closingComplete: ClosingComplete, closingFeerate: FeeratePerKw)

sealed trait OnRemoteShutdown
object OnRemoteShutdown {
/** When receiving the remote shutdown, we sign a new version of our closing transaction. */
case class SignTransaction(closingFeerate: FeeratePerKw) extends OnRemoteShutdown
/** When receiving the remote shutdown, we don't sign a new version of our closing transaction, but our peer may sign theirs. */
case object WaitForSigs extends OnRemoteShutdown
}

sealed trait ClosingNegotiation {
def localShutdown: Shutdown
// When we disconnect, we discard pending signatures.
def disconnect(): ClosingNegotiation.WaitingForRemoteShutdown = this match {
case status: ClosingNegotiation.WaitingForRemoteShutdown => status
case status: ClosingNegotiation.SigningTransactions => status.closingCompleteSent_opt.map(_.closingFeerate) match {
// If we were waiting for their signature, we will send closing_complete again after exchanging shutdown.
case Some(closingFeerate) if status.closingSigReceived_opt.isEmpty => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.SignTransaction(closingFeerate))
case _ => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.WaitForSigs)
}
case status: ClosingNegotiation.WaitingForConfirmation => ClosingNegotiation.WaitingForRemoteShutdown(status.localShutdown, OnRemoteShutdown.WaitForSigs)
}
}
object ClosingNegotiation {
/** We've sent a new shutdown message: we wait for their shutdown message before taking any action. */
case class WaitingForRemoteShutdown(localShutdown: Shutdown, onRemoteShutdown: OnRemoteShutdown) extends ClosingNegotiation
/** We've exchanged shutdown messages: at least one side will send closing_complete to renew their closing transaction. */
case class SigningTransactions(localShutdown: Shutdown, remoteShutdown: Shutdown, closingCompleteSent_opt: Option[ClosingCompleteSent], closingSigSent_opt: Option[ClosingSig], closingSigReceived_opt: Option[ClosingSig]) extends ClosingNegotiation
/** We've signed a new closing transaction and are waiting for confirmation or to initiate RBF. */
case class WaitingForConfirmation(localShutdown: Shutdown, remoteShutdown: Shutdown) extends ClosingNegotiation
}

sealed trait ChannelData extends PossiblyHarmful {
def channelId: ByteVector32
}
Expand Down Expand Up @@ -655,12 +687,13 @@ final case class DATA_NEGOTIATING(commitments: Commitments,
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")
}
final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown,
status: ClosingNegotiation,
// Closing transactions we created, where we pay the fees (unsigned).
proposedClosingTxs: List[ClosingTxs],
// Closing transactions we published: this contains our local transactions for
// which they sent a signature, and their closing transactions that we signed.
publishedClosingTxs: List[ClosingTx]) extends ChannelDataWithCommitments {
val localScriptPubKey: ByteVector = status.localShutdown.scriptPubKey
def findClosingTx(tx: Transaction): Option[ClosingTx] = publishedClosingTxs.find(_.tx.txid == tx.txid).orElse(proposedClosingTxs.flatMap(_.all).find(_.tx.txid == tx.txid))
}
final case class DATA_CLOSING(commitments: Commitments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ case class InvalidHtlcSignature (override val channelId: Byte
case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed")
case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output")
case class InvalidCloseSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid close signature: txId=$txId")
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")
case class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid closing tx: some outputs are below dust: txId=$txId")
case class CommitSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"commit sig count mismatch: expected=$expected actual=$actual")
case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual=$actual")
Expand Down
Loading

0 comments on commit ea979aa

Please sign in to comment.