Skip to content

Commit 620612f

Browse files
committed
Adapt the simple_close protocol to simple taproot channels
`partial_signature_with_nonce` TLVs are added to `closing_complete` and `closing_sig` with the same format as in `commitment_signed` The closing workflow is similar to the standard "simple close" workflow: - Alice and Bob exchange `shutdown`, which includes a "closing nonce" (no changes here compared to the "simple taproot channels" spec). - Alice selects possible closing transaction (closer_output_only, closee_output_only, closer_and_closee_output) and for each of them creates a partial_signature_with_nonce using a new random local nonce and Bob's closing nonce (which she received in Bob's `shutdown` message). - Alice send a `closing_complete` message to Bob that include these partial_signature_with_nonce. - Bob receive Alice's `closing_complete` message, selects one of Alice's partial_signature_with_nonce, creates partial_signature_with_nonce using. his closing nonce and the nonce attached to the partial_signature_with_nonce and sends it to Alice in a `closing_sig` message. - Alice receives Bob's `closing_sig` and creates a partial signature for her closing tx using her closing nonce and the nonce attached Bob's partial_signature_with_nonce. - Alice combines this signature with Bob's and can broadcat her closing tx.
1 parent 1de0aa1 commit 620612f

File tree

7 files changed

+237
-51
lines changed

7 files changed

+237
-51
lines changed

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

Lines changed: 150 additions & 44 deletions
Large diffs are not rendered by default.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
773773
if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) {
774774
// there are no pending signed changes, let's directly negotiate a closing transaction
775775
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
776-
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, sendList)
776+
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, sendList, closingNonce, remoteShutdown.shutdownNonce_opt)
777777
} else if (d.commitments.params.localParams.paysClosingFees) {
778778
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
779779
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.closingFeerates, closingNonce, remoteShutdown.shutdownNonce_opt)
@@ -1592,7 +1592,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
15921592
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
15931593
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
15941594
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
1595-
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, revocation :: Nil)
1595+
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, revocation :: Nil, closingNonce, remoteShutdown.shutdownNonce_opt)
15961596
} else if (d.commitments.params.localParams.paysClosingFees) {
15971597
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
15981598
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates, closingNonce, remoteShutdown.shutdownNonce_opt)
@@ -1636,7 +1636,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
16361636
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
16371637
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
16381638
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
1639-
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, Nil)
1639+
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, Nil, closingNonce, remoteShutdown.shutdownNonce_opt)
16401640
} else if (d.commitments.params.localParams.paysClosingFees) {
16411641
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
16421642
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates, closingNonce, remoteShutdown.shutdownNonce_opt)
@@ -1871,7 +1871,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
18711871
case status: ClosingNegotiation.SigningTransactions =>
18721872
val localScript = status.localShutdown.scriptPubKey
18731873
val remoteScript = status.remoteShutdown.scriptPubKey
1874-
MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, localScript, remoteScript, closingComplete) match {
1874+
MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, localScript, remoteScript, closingComplete, closingNonce) match {
18751875
case Left(f) =>
18761876
// This may happen if scripts were updated concurrently, so we simply ignore failures.
18771877
log.warning("invalid closing_complete: {}", f.getMessage)

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package fr.acinq.eclair.channel.fsm
1818

1919
import akka.actor.FSM
20+
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
2021
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Script}
2122
import fr.acinq.eclair.Features
2223
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
@@ -132,11 +133,11 @@ trait CommonHandlers {
132133
finalScriptPubKey
133134
}
134135

135-
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates], toSend: List[LightningMessage]) = {
136+
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates], toSend: List[LightningMessage], localClosingNonce_opt: Option[(SecretNonce, IndividualNonce)] = None, remoteClosingNonce_opt: Option[IndividualNonce] = None) = {
136137
val localScript = localShutdown.scriptPubKey
137138
val remoteScript = remoteShutdown.scriptPubKey
138139
val closingFeerate = closingFeerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
139-
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match {
140+
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate, localClosingNonce_opt, remoteClosingNonce_opt) match {
140141
case Left(f) =>
141142
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
142143
val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None)

eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,11 @@ super.sign(privateKey, txOwner, commitmentFormat)
374374
}
375375
}
376376

377-
case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { override def desc: String = "closing" }
377+
case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo {
378+
// these nonces are generated on the fly at during a "simple" closing session and can be forgotten once the session ends
379+
@volatile var localNonce_opt: Option[(SecretNonce, IndividualNonce)] = None
380+
override def desc: String = "closing"
381+
}
378382

379383
sealed trait TxGenerationSkipped
380384
case object OutputNotFound extends TxGenerationSkipped { override def toString = "output not found (probably trimmed)" }

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,22 @@ object ClosingTlv {
321321
/** Signature for a closing transaction containing the closer and closee's outputs. */
322322
case class CloserAndClosee(sig: ByteVector64) extends ClosingTlv
323323

324+
/** Signature for a closing transaction containing only the closer's output. */
325+
case class CloserNoCloseePartialSignature(partialSigWithNonce: PartialSignatureWithNonce) extends ClosingTlv
326+
327+
/** Signature for a closing transaction containing only the closee's output. */
328+
case class NoCloserCloseePartialSignature(partialSigWithNonce: PartialSignatureWithNonce) extends ClosingTlv
329+
330+
/** Signature for a closing transaction containing the closer and closee's outputs. */
331+
case class CloserAndCloseePartialSignature(partialSigWithNonce: PartialSignatureWithNonce) extends ClosingTlv
332+
324333
val closingTlvCodec: Codec[TlvStream[ClosingTlv]] = tlvStream(discriminated[ClosingTlv].by(varint)
325334
.typecase(UInt64(1), tlvField(bytes64.as[CloserNoClosee]))
326335
.typecase(UInt64(2), tlvField(bytes64.as[NoCloserClosee]))
327336
.typecase(UInt64(3), tlvField(bytes64.as[CloserAndClosee]))
337+
.typecase(UInt64(4), tlvField(partialSignatureWithNonce.as[CloserNoCloseePartialSignature]))
338+
.typecase(UInt64(5), tlvField(partialSignatureWithNonce.as[NoCloserCloseePartialSignature]))
339+
.typecase(UInt64(6), tlvField(partialSignatureWithNonce.as[CloserAndCloseePartialSignature]))
328340
)
329341

330342
}

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,18 @@ case class ClosingComplete(channelId: ByteVector32, fees: Satoshi, lockTime: Lon
417417
val closerNoCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserNoClosee].map(_.sig)
418418
val noCloserCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.NoCloserClosee].map(_.sig)
419419
val closerAndCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndClosee].map(_.sig)
420+
val closerNoCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserNoCloseePartialSignature].map(_.partialSigWithNonce)
421+
val noCloserCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.NoCloserCloseePartialSignature].map(_.partialSigWithNonce)
422+
val closerAndCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserAndCloseePartialSignature].map(_.partialSigWithNonce)
420423
}
421424

422425
case class ClosingSig(channelId: ByteVector32, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
423426
val closerNoCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserNoClosee].map(_.sig)
424427
val noCloserCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.NoCloserClosee].map(_.sig)
425428
val closerAndCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndClosee].map(_.sig)
429+
val closerNoCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserNoCloseePartialSignature].map(_.partialSigWithNonce)
430+
val noCloserCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.NoCloserCloseePartialSignature].map(_.partialSigWithNonce)
431+
val closerAndCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserAndCloseePartialSignature].map(_.partialSigWithNonce)
426432
}
427433

428434
case class UpdateAddHtlc(channelId: ByteVector32,

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,42 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
518518
assert(bob.stateName == NEGOTIATING_SIMPLE)
519519
}
520520

521+
test("recv ClosingComplete (both outputs, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
522+
import f._
523+
aliceClose(f)
524+
val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete]
525+
assert(aliceClosingComplete.fees > 0.sat)
526+
assert(aliceClosingComplete.closerAndCloseeSig_opt.nonEmpty || aliceClosingComplete.closerAndCloseePartialSig_opt.nonEmpty)
527+
assert(aliceClosingComplete.closerNoCloseeSig_opt.nonEmpty || aliceClosingComplete.closerNoCloseePartialSig_opt.nonEmpty)
528+
assert(aliceClosingComplete.noCloserCloseeSig_opt.isEmpty && aliceClosingComplete.noCloserCloseePartialSig_opt.isEmpty)
529+
val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete]
530+
assert(bobClosingComplete.fees > 0.sat)
531+
assert(bobClosingComplete.closerAndCloseeSig_opt.nonEmpty || bobClosingComplete.closerAndCloseePartialSig_opt.nonEmpty)
532+
assert(bobClosingComplete.closerNoCloseeSig_opt.nonEmpty || bobClosingComplete.closerNoCloseePartialSig_opt.nonEmpty)
533+
assert(bobClosingComplete.noCloserCloseeSig_opt.isEmpty && bobClosingComplete.noCloserCloseePartialSig_opt.isEmpty)
534+
535+
alice2bob.forward(bob, aliceClosingComplete)
536+
val bobClosingSig = bob2alice.expectMsgType[ClosingSig]
537+
bob2alice.forward(alice, bobClosingSig)
538+
val aliceTx = alice2blockchain.expectMsgType[PublishFinalTx]
539+
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx.tx.txid)
540+
assert(aliceTx.desc == "closing")
541+
alice2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid)
542+
bob2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid)
543+
assert(alice.stateName == NEGOTIATING_SIMPLE)
544+
545+
bob2alice.forward(alice, bobClosingComplete)
546+
val aliceClosingSig = alice2bob.expectMsgType[ClosingSig]
547+
alice2bob.forward(bob, aliceClosingSig)
548+
val bobTx = bob2blockchain.expectMsgType[PublishFinalTx]
549+
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTx.tx.txid)
550+
assert(aliceTx.tx.txid != bobTx.tx.txid)
551+
assert(bobTx.desc == "closing")
552+
bob2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
553+
alice2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
554+
assert(bob.stateName == NEGOTIATING_SIMPLE)
555+
}
556+
521557
test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f =>
522558
import f._
523559
aliceClose(f)
@@ -539,6 +575,27 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
539575
assert(bob.stateName == NEGOTIATING_SIMPLE)
540576
}
541577

578+
test("recv ClosingComplete (single output, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount), Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
579+
import f._
580+
aliceClose(f)
581+
val closingComplete = alice2bob.expectMsgType[ClosingComplete]
582+
assert(closingComplete.closerAndCloseeSig_opt.isEmpty && closingComplete.closerAndCloseePartialSig_opt.isEmpty)
583+
assert(closingComplete.closerNoCloseeSig_opt.nonEmpty || closingComplete.closerNoCloseePartialSig_opt.nonEmpty)
584+
assert(closingComplete.noCloserCloseeSig_opt.isEmpty && closingComplete.noCloserCloseePartialSig_opt.isEmpty)
585+
// Bob has nothing at stake.
586+
bob2alice.expectNoMessage(100 millis)
587+
588+
alice2bob.forward(bob, closingComplete)
589+
bob2alice.expectMsgType[ClosingSig]
590+
bob2alice.forward(alice)
591+
val closingTx = alice2blockchain.expectMsgType[PublishFinalTx]
592+
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid)
593+
alice2blockchain.expectWatchTxConfirmed(closingTx.tx.txid)
594+
bob2blockchain.expectWatchTxConfirmed(closingTx.tx.txid)
595+
assert(alice.stateName == NEGOTIATING_SIMPLE)
596+
assert(bob.stateName == NEGOTIATING_SIMPLE)
597+
}
598+
542599
test("recv ClosingComplete (single output, trimmed)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f =>
543600
import f._
544601
val (r, htlc) = addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice)

0 commit comments

Comments
 (0)