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 9, 2024
1 parent b625aba commit df6ad3e
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 113 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
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,6 @@ object Helpers {

/** We are the closer: we sign closing transactions for which we pay the fees. */
def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = {
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid localScriptPubkey")
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid remoteScriptPubkey")
// We must convert the feerate to a fee: we must build dummy transactions to compute their weight.
val closingFee = {
val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
Expand Down
Loading

0 comments on commit df6ad3e

Please sign in to comment.