1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 01:43:22 +01:00

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.
This commit is contained in:
sstone 2024-10-17 15:21:35 +02:00
parent c50ab108b1
commit f224b9a302
No known key found for this signature in database
GPG Key ID: E04E48E72C205463
7 changed files with 237 additions and 51 deletions

View File

@ -749,7 +749,7 @@ 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)] = {
def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, localClosingNonce_opt: Option[(SecretNonce, IndividualNonce)] = None, remoteClosingNonce_opt: Option[IndividualNonce] = None): 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.
@ -763,18 +763,61 @@ object Helpers {
}
}
// Now that we know the fee we're ready to pay, we can create our closing transactions.
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
def freshLocalNonce() = keyManager.signingNonce(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
def addFreshLocalNonce(input: ClosingTx): ClosingTx = {
input.localNonce_opt = Some(freshLocalNonce())
input
}
val closingTxs = {
val txs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
if (commitment.params.commitmentFormat.useTaproot) {
// for taproot channels, for each closing tx we generate a new fresh random nonce that we'll attach to the closing tx (so we can remember it) and will use to create a partial signature
txs.copy(
localAndRemote_opt = txs.localAndRemote_opt.map(addFreshLocalNonce),
localOnly_opt = txs.localOnly_opt.map(addFreshLocalNonce),
remoteOnly_opt = txs.remoteOnly_opt.map(addFreshLocalNonce)
)
} else {
txs
}
}
// The actual fee we're paying will be bigger than the one we previously computed if we omit our output.
val actualFee = closingTxs.preferred_opt match {
case Some(closingTx) if closingTx.fee > 0.sat => closingTx.fee
case _ => return Left(CannotGenerateClosingTx(commitment.channelId))
}
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val closingComplete = ClosingComplete(commitment.channelId, actualFee, currentBlockHeight.toLong, TlvStream(Set(
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserNoClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.NoCloserClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
).flatten[ClosingTlv]))
val signatures = if (commitment.params.commitmentFormat.useTaproot) {
val Some(remoteClosingNonce) = remoteClosingNonce_opt
def sign(tx: ClosingTx): PartialSignatureWithNonce = {
// we use the new local nonce we just created, and the remote closing nonce we received in their shutdown message
val Right(localClosingPartialSig) = keyManager.partialSign(
tx,
keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex), commitment.remoteFundingPubKey,
TxOwner.Local,
tx.localNonce_opt.get, remoteClosingNonce,
)
PartialSignatureWithNonce(localClosingPartialSig, tx.localNonce_opt.get._2)
}
Set(
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseePartialSignature(sign(tx))),
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserNoCloseePartialSignature(sign(tx))),
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.NoCloserCloseePartialSignature(sign(tx))),
)
} else {
Set(
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserNoClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.NoCloserClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
)
}
val closingComplete = ClosingComplete(commitment.channelId, actualFee, currentBlockHeight.toLong, TlvStream(signatures.flatten[ClosingTlv]))
Right(closingTxs, closingComplete)
}
@ -783,33 +826,71 @@ object Helpers {
* Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that the
* closing_complete doesn't match the latest state of the closing negotiation (someone changed their script).
*/
def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = {
def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete, localClosingNonce_opt: Option[(SecretNonce, IndividualNonce)]): Either[ChannelException, (ClosingTx, ClosingSig)] = {
val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees)
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey)
// If our output isn't dust, they must provide a signature for a transaction that includes it.
// Note that we're the closee, so we look for signatures including the closee output.
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
case (Some(_), Some(_)) if closingComplete.closerAndCloseeSig_opt.isEmpty && closingComplete.noCloserCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (Some(_), None) if closingComplete.closerAndCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (None, Some(_)) if closingComplete.noCloserCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case _ => ()
}
// We choose the closing signature that matches our preferred closing transaction.
val closingTxsWithSigs = Seq(
closingComplete.closerAndCloseeSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndClosee(localSig)))),
closingComplete.noCloserCloseeSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.NoCloserClosee(localSig)))),
closingComplete.closerNoCloseeSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserNoClosee(localSig)))),
).flatten
closingTxsWithSigs.headOption match {
case Some((closingTx, remoteSig, sigToTlv)) =>
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
Transactions.checkSpendable(signedClosingTx) match {
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, TlvStream(sigToTlv(localSig))))
if (commitment.params.commitmentFormat.useTaproot) {
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
case (Some(_), Some(_)) if closingComplete.closerAndCloseePartialSig_opt.isEmpty && closingComplete.noCloserCloseePartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (Some(_), None) if closingComplete.closerAndCloseePartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (None, Some(_)) if closingComplete.noCloserCloseePartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case _ => ()
}
val closingTxsWithSigs = Seq(
closingComplete.closerAndCloseePartialSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseePartialSignature(localSig)))),
closingComplete.noCloserCloseePartialSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.NoCloserCloseePartialSignature(localSig)))),
closingComplete.closerNoCloseePartialSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserNoCloseePartialSignature(localSig)))),
).flatten
// We choose the closing signature that matches our preferred closing transaction.
closingTxsWithSigs.headOption match {
case Some((closingTx, remoteSig, sigToTlv)) =>
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
// we use our local closing nonce, that we included in our shutdown message, and the nonce they sent with their partial signature
// this is the first and only time we'll use our closing nonce (we used a fresh random nonce when we sent closing_complete)
val Right(localSig) = keyManager.partialSign(closingTx, localFundingPubKey, commitment.remoteFundingPubKey, TxOwner.Local, localClosingNonce_opt.get, remoteSig.nonce)
val Right(aggSig) = Musig2.aggregateTaprootSignatures(
Seq(localSig, remoteSig.partialSig), closingTx.tx, closingTx.tx.txIn.indexWhere(_.outPoint == closingTx.input.outPoint),
Seq(closingTx.input.txOut),
Scripts.sort(Seq(localFundingPubKey.publicKey, commitment.remoteFundingPubKey)),
Seq(localClosingNonce_opt.get._2, remoteSig.nonce),
None)
val signedClosingTx = Transactions.addAggregatedSignature(closingTx, aggSig)
Transactions.checkSpendable(signedClosingTx) match {
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, TlvStream(sigToTlv(PartialSignatureWithNonce(localSig, localClosingNonce_opt.get._2)))))
}
case None => Left(MissingCloseSignature(commitment.channelId))
}
} else {
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
case (Some(_), Some(_)) if closingComplete.closerAndCloseeSig_opt.isEmpty && closingComplete.noCloserCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (Some(_), None) if closingComplete.closerAndCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (None, Some(_)) if closingComplete.noCloserCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case _ => ()
}
val closingTxsWithSigs = Seq(
closingComplete.closerAndCloseeSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndClosee(localSig)))),
closingComplete.noCloserCloseeSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.NoCloserClosee(localSig)))),
closingComplete.closerNoCloseeSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserNoClosee(localSig)))),
).flatten
// We choose the closing signature that matches our preferred closing transaction.
closingTxsWithSigs.headOption match {
case Some((closingTx, remoteSig, sigToTlv)) =>
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
Transactions.checkSpendable(signedClosingTx) match {
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, TlvStream(sigToTlv(localSig))))
}
case None => {
Left(MissingCloseSignature(commitment.channelId))
}
case None => Left(MissingCloseSignature(commitment.channelId))
}
}
}
@ -819,21 +900,46 @@ object Helpers {
* closing_sig doesn't match the latest state of the closing negotiation (someone changed their script).
*/
def receiveSimpleClosingSig(keyManager: ChannelKeyManager, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig): Either[ChannelException, ClosingTx] = {
val closingTxsWithSig = Seq(
closingSig.closerAndCloseeSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))),
closingSig.closerNoCloseeSig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))),
closingSig.noCloserCloseeSig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))),
).flatten
closingTxsWithSig.headOption match {
case Some((closingTx, remoteSig)) =>
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
Transactions.checkSpendable(signedClosingTx) match {
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
case Success(_) => Right(signedClosingTx)
}
case None => Left(MissingCloseSignature(commitment.channelId))
if (commitment.params.commitmentFormat.useTaproot) {
val closingTxsWithSig = Seq(
closingSig.closerAndCloseePartialSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))),
closingSig.closerNoCloseePartialSig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))),
closingSig.noCloserCloseePartialSig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))),
).flatten
closingTxsWithSig.headOption match {
case Some((closingTx, remoteSig)) =>
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val Right(localSig) = keyManager.partialSign(closingTx, localFundingPubKey, commitment.remoteFundingPubKey, TxOwner.Local, closingTx.localNonce_opt.get, remoteSig.nonce)
val Right(aggSig) = Musig2.aggregateTaprootSignatures(
Seq(localSig, remoteSig.partialSig), closingTx.tx, closingTx.tx.txIn.indexWhere(_.outPoint == closingTx.input.outPoint),
Seq(closingTx.input.txOut),
Scripts.sort(Seq(localFundingPubKey.publicKey, commitment.remoteFundingPubKey)),
Seq(closingTx.localNonce_opt.get._2, remoteSig.nonce),
None)
val signedClosingTx = Transactions.addAggregatedSignature(closingTx, aggSig)
Transactions.checkSpendable(signedClosingTx) match {
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
case Success(_) => Right(signedClosingTx)
}
case None => Left(MissingCloseSignature(commitment.channelId))
}
} else {
val closingTxsWithSig = Seq(
closingSig.closerAndCloseeSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))),
closingSig.closerNoCloseeSig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))),
closingSig.noCloserCloseeSig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))),
).flatten
closingTxsWithSig.headOption match {
case Some((closingTx, remoteSig)) =>
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
Transactions.checkSpendable(signedClosingTx) match {
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
case Success(_) => Right(signedClosingTx)
}
case None => Left(MissingCloseSignature(commitment.channelId))
}
}
}

View File

@ -765,7 +765,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) {
// there are no pending signed changes, let's directly negotiate a closing transaction
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, sendList)
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, sendList, closingNonce, remoteShutdown.shutdownNonce_opt)
} else if (d.commitments.params.localParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.closingFeerates, closingNonce, remoteShutdown.shutdownNonce_opt)
@ -1573,7 +1573,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, revocation :: Nil)
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, revocation :: Nil, closingNonce, remoteShutdown.shutdownNonce_opt)
} else if (d.commitments.params.localParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates, closingNonce, remoteShutdown.shutdownNonce_opt)
@ -1617,7 +1617,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, Nil)
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, Nil, closingNonce, remoteShutdown.shutdownNonce_opt)
} else if (d.commitments.params.localParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates, closingNonce, remoteShutdown.shutdownNonce_opt)
@ -1852,7 +1852,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case status: ClosingNegotiation.SigningTransactions =>
val localScript = status.localShutdown.scriptPubKey
val remoteScript = status.remoteShutdown.scriptPubKey
MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, localScript, remoteScript, closingComplete) match {
MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, localScript, remoteScript, closingComplete, closingNonce) match {
case Left(f) =>
// This may happen if scripts were updated concurrently, so we simply ignore failures.
log.warning("invalid closing_complete: {}", f.getMessage)

View File

@ -17,6 +17,7 @@
package fr.acinq.eclair.channel.fsm
import akka.actor.FSM
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Script}
import fr.acinq.eclair.Features
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
@ -132,11 +133,11 @@ trait CommonHandlers {
finalScriptPubKey
}
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates], toSend: List[LightningMessage]) = {
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) = {
val localScript = localShutdown.scriptPubKey
val remoteScript = remoteShutdown.scriptPubKey
val closingFeerate = closingFeerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match {
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate, localClosingNonce_opt, remoteClosingNonce_opt) match {
case Left(f) =>
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
val status = ClosingNegotiation.SigningTransactions(localShutdown, remoteShutdown, None, None, None)

View File

@ -374,7 +374,11 @@ super.sign(privateKey, txOwner, commitmentFormat)
}
}
case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { override def desc: String = "closing" }
case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo {
// these nonces are generated on the fly at during a "simple" closing session and can be forgotten once the session ends
@volatile var localNonce_opt: Option[(SecretNonce, IndividualNonce)] = None
override def desc: String = "closing"
}
sealed trait TxGenerationSkipped
case object OutputNotFound extends TxGenerationSkipped { override def toString = "output not found (probably trimmed)" }

View File

@ -321,10 +321,22 @@ object ClosingTlv {
/** Signature for a closing transaction containing the closer and closee's outputs. */
case class CloserAndClosee(sig: ByteVector64) extends ClosingTlv
/** Signature for a closing transaction containing only the closer's output. */
case class CloserNoCloseePartialSignature(partialSigWithNonce: PartialSignatureWithNonce) extends ClosingTlv
/** Signature for a closing transaction containing only the closee's output. */
case class NoCloserCloseePartialSignature(partialSigWithNonce: PartialSignatureWithNonce) extends ClosingTlv
/** Signature for a closing transaction containing the closer and closee's outputs. */
case class CloserAndCloseePartialSignature(partialSigWithNonce: PartialSignatureWithNonce) extends ClosingTlv
val closingTlvCodec: Codec[TlvStream[ClosingTlv]] = tlvStream(discriminated[ClosingTlv].by(varint)
.typecase(UInt64(1), tlvField(bytes64.as[CloserNoClosee]))
.typecase(UInt64(2), tlvField(bytes64.as[NoCloserClosee]))
.typecase(UInt64(3), tlvField(bytes64.as[CloserAndClosee]))
.typecase(UInt64(4), tlvField(partialSignatureWithNonce.as[CloserNoCloseePartialSignature]))
.typecase(UInt64(5), tlvField(partialSignatureWithNonce.as[NoCloserCloseePartialSignature]))
.typecase(UInt64(6), tlvField(partialSignatureWithNonce.as[CloserAndCloseePartialSignature]))
)
}

View File

@ -399,12 +399,18 @@ case class ClosingComplete(channelId: ByteVector32, fees: Satoshi, lockTime: Lon
val closerNoCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserNoClosee].map(_.sig)
val noCloserCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.NoCloserClosee].map(_.sig)
val closerAndCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndClosee].map(_.sig)
val closerNoCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserNoCloseePartialSignature].map(_.partialSigWithNonce)
val noCloserCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.NoCloserCloseePartialSignature].map(_.partialSigWithNonce)
val closerAndCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserAndCloseePartialSignature].map(_.partialSigWithNonce)
}
case class ClosingSig(channelId: ByteVector32, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val closerNoCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserNoClosee].map(_.sig)
val noCloserCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.NoCloserClosee].map(_.sig)
val closerAndCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndClosee].map(_.sig)
val closerNoCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserNoCloseePartialSignature].map(_.partialSigWithNonce)
val noCloserCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.NoCloserCloseePartialSignature].map(_.partialSigWithNonce)
val closerAndCloseePartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingTlv.CloserAndCloseePartialSignature].map(_.partialSigWithNonce)
}
case class UpdateAddHtlc(channelId: ByteVector32,

View File

@ -518,6 +518,42 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(bob.stateName == NEGOTIATING_SIMPLE)
}
test("recv ClosingComplete (both outputs, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
aliceClose(f)
val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete]
assert(aliceClosingComplete.fees > 0.sat)
assert(aliceClosingComplete.closerAndCloseeSig_opt.nonEmpty || aliceClosingComplete.closerAndCloseePartialSig_opt.nonEmpty)
assert(aliceClosingComplete.closerNoCloseeSig_opt.nonEmpty || aliceClosingComplete.closerNoCloseePartialSig_opt.nonEmpty)
assert(aliceClosingComplete.noCloserCloseeSig_opt.isEmpty && aliceClosingComplete.noCloserCloseePartialSig_opt.isEmpty)
val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete]
assert(bobClosingComplete.fees > 0.sat)
assert(bobClosingComplete.closerAndCloseeSig_opt.nonEmpty || bobClosingComplete.closerAndCloseePartialSig_opt.nonEmpty)
assert(bobClosingComplete.closerNoCloseeSig_opt.nonEmpty || bobClosingComplete.closerNoCloseePartialSig_opt.nonEmpty)
assert(bobClosingComplete.noCloserCloseeSig_opt.isEmpty && bobClosingComplete.noCloserCloseePartialSig_opt.isEmpty)
alice2bob.forward(bob, aliceClosingComplete)
val bobClosingSig = bob2alice.expectMsgType[ClosingSig]
bob2alice.forward(alice, bobClosingSig)
val aliceTx = alice2blockchain.expectMsgType[PublishFinalTx]
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx.tx.txid)
assert(aliceTx.desc == "closing")
alice2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid)
bob2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid)
assert(alice.stateName == NEGOTIATING_SIMPLE)
bob2alice.forward(alice, bobClosingComplete)
val aliceClosingSig = alice2bob.expectMsgType[ClosingSig]
alice2bob.forward(bob, aliceClosingSig)
val bobTx = bob2blockchain.expectMsgType[PublishFinalTx]
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTx.tx.txid)
assert(aliceTx.tx.txid != bobTx.tx.txid)
assert(bobTx.desc == "closing")
bob2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
alice2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
assert(bob.stateName == NEGOTIATING_SIMPLE)
}
test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f =>
import f._
aliceClose(f)
@ -539,6 +575,27 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(bob.stateName == NEGOTIATING_SIMPLE)
}
test("recv ClosingComplete (single output, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount), Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
aliceClose(f)
val closingComplete = alice2bob.expectMsgType[ClosingComplete]
assert(closingComplete.closerAndCloseeSig_opt.isEmpty && closingComplete.closerAndCloseePartialSig_opt.isEmpty)
assert(closingComplete.closerNoCloseeSig_opt.nonEmpty || closingComplete.closerNoCloseePartialSig_opt.nonEmpty)
assert(closingComplete.noCloserCloseeSig_opt.isEmpty && closingComplete.noCloserCloseePartialSig_opt.isEmpty)
// Bob has nothing at stake.
bob2alice.expectNoMessage(100 millis)
alice2bob.forward(bob, closingComplete)
bob2alice.expectMsgType[ClosingSig]
bob2alice.forward(alice)
val closingTx = alice2blockchain.expectMsgType[PublishFinalTx]
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid)
alice2blockchain.expectWatchTxConfirmed(closingTx.tx.txid)
bob2blockchain.expectWatchTxConfirmed(closingTx.tx.txid)
assert(alice.stateName == NEGOTIATING_SIMPLE)
assert(bob.stateName == NEGOTIATING_SIMPLE)
}
test("recv ClosingComplete (single output, trimmed)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f =>
import f._
val (r, htlc) = addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice)