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:
parent
c50ab108b1
commit
f224b9a302
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)" }
|
||||
|
@ -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]))
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user