diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 59d0dfca9..755e71222 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -134,7 +134,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, min = (commitmentFeerate * feerateTolerance.ratioLow).max(minimumFeerate), max = (commitmentFormat match { case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh - case _: Transactions.AnchorOutputsCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate) + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate) }).max(minimumFeerate), ) RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala index 874bdbd08..5b9f6bff4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala @@ -121,7 +121,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets, commitmentFormat match { case Transactions.DefaultCommitmentFormat => networkFeerate - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) // We make sure the feerate is always greater than the propagation threshold. targetFeerate.max(networkMinFee * 1.25) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index a3c6d163f..4d31cbb55 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -357,20 +357,22 @@ object Helpers { } object Funding { - def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): ByteVector = if (commitmentFormat.useTaproot) { - write(Taproot.musig2FundingScript(localFundingKey, remoteFundingKey)) - } else { - write(pay2wsh(multiSig2of2(localFundingKey, remoteFundingKey))) + def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): ByteVector = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + write(Taproot.musig2FundingScript(localFundingKey, remoteFundingKey)) + case _ => + write(pay2wsh(multiSig2of2(localFundingKey, remoteFundingKey))) } - def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey, commitmentFormat: CommitmentFormat): InputInfo = if (commitmentFormat.useTaproot) { - val fundingScript = Taproot.musig2FundingScript(fundingPubkey1, fundingPubkey2) - val fundingTxOut = TxOut(fundingSatoshis, fundingScript) - InputInfo.TaprootInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, Taproot.musig2Aggregate(fundingPubkey1, fundingPubkey2), None) - } else { - val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) - val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) - InputInfo.SegwitInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript)) + def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey, commitmentFormat: CommitmentFormat): InputInfo = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val fundingScript = Taproot.musig2FundingScript(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingSatoshis, fundingScript) + InputInfo.TaprootInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, Taproot.musig2Aggregate(fundingPubkey1, fundingPubkey2), None) + case _ => + val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) + InputInfo.SegwitInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript)) } /** @@ -661,7 +663,7 @@ object Helpers { case DefaultCommitmentFormat => // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" requestedFeerate.min(commitment.localCommit.spec.commitTxFeerate) - case _: AnchorOutputsCommitmentFormat => requestedFeerate + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => requestedFeerate } // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. @@ -892,18 +894,18 @@ object Helpers { def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, lcp: LocalCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): LocalCommitPublished = { if (shouldUpdateAnchorTxs(lcp.claimAnchorTxs, confirmationTarget)) { val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey - val localPaymentKey = if (commitment.params.commitmentFormat.useTaproot) { - val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitment.localCommit.index) - val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - localDelayedPaymentPubkey - } else { - localFundingPubKey + val localPaymentKey = commitment.params.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitment.localCommit.index) + val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + localDelayedPaymentPubkey + case _ => + localFundingPubKey } - val remotePaymentKey = if (commitment.params.commitmentFormat.useTaproot) { - commitment.remoteParams.paymentBasepoint - } else { - commitment.remoteFundingPubKey + val remotePaymentKey = commitment.params.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => commitment.remoteParams.paymentBasepoint + case _ => commitment.remoteFundingPubKey } val claimAnchorTxs = List( withTxGenerationLog("local-anchor") { @@ -1028,17 +1030,19 @@ object Helpers { val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey // taproot channels do not re-use the funding pubkeys for anchor outputs - val localPaymentKey = if (commitment.params.commitmentFormat.useTaproot) { - val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - commitment.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - } else { - localFundingPubkey + val localPaymentKey = commitment.params.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) + commitment.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + case _ => + localFundingPubkey } - val remotePaymentKey = if (commitment.params.commitmentFormat.useTaproot) { - val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitment.remoteParams.delayedPaymentBasepoint, commitment.remoteCommit.remotePerCommitmentPoint) - remoteDelayedPaymentPubkey - } else { - commitment.remoteFundingPubKey + val remotePaymentKey = commitment.params.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitment.remoteParams.delayedPaymentBasepoint, commitment.remoteCommit.remotePerCommitmentPoint) + remoteDelayedPaymentPubkey + case _ => + commitment.remoteFundingPubKey } val claimAnchorTxs = List( withTxGenerationLog("local-anchor") { @@ -1078,7 +1082,7 @@ object Helpers { Transactions.addSigs(claimMain, localPubkey, sig) }) } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(tx, params.localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feeratePerKwMain).map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat) Transactions.addSigs(claimMain, sig) @@ -1214,7 +1218,7 @@ object Helpers { Transactions.addSigs(claimMain, localPaymentPubkey, sig) }) } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feerateMain).map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) Transactions.addSigs(claimMain, sig) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index a13e8ea34..a0c9d2c15 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -237,7 +237,7 @@ trait ErrorHandlers extends CommonHandlers { case Transactions.DefaultCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitment, commitTx)) val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx if !localCommitPublished.isConfirmed => PublishReplaceableTx(tx, commitment, commitTx) } List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index b8969f469..9d19fbdf5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_ import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector @@ -44,7 +44,7 @@ object Scripts { private def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match { case DefaultCommitmentFormat => SIGHASH_ALL - case _: AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } /** Sort public keys using lexicographic ordering. */ @@ -191,6 +191,7 @@ object Scripts { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false case _: AnchorOutputsCommitmentFormat => true + case SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key @@ -243,6 +244,7 @@ object Scripts { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false case _: AnchorOutputsCommitmentFormat => true + case SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index cc6e1e3ac..0769a81f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -52,7 +52,6 @@ object Transactions { def htlcSuccessWeight: Int def htlcTimeoutInputWeight: Int def htlcSuccessInputWeight: Int - def useTaproot: Boolean = false // @formatter:on } @@ -98,9 +97,13 @@ object Transactions { */ case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat - case object SimpleTaprootChannelCommitmentFormat extends AnchorOutputsCommitmentFormat { + case object SimpleTaprootChannelCommitmentFormat extends CommitmentFormat { override val commitWeight = 968 - override val useTaproot = true + override val htlcOutputWeight = 172 + override val htlcTimeoutWeight = 666 + override val htlcSuccessWeight = 706 + override val htlcTimeoutInputWeight = 452 + override val htlcSuccessInputWeight = 491 } // @formatter:off @@ -144,7 +147,10 @@ object Transactions { Satoshi(FeeratePerKw.MinimumRelayFeeRate * vsize / 1000) } /** Sighash flags to use when signing the transaction. */ - def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = if (input.isP2tr) SIGHASH_DEFAULT else SIGHASH_ALL + def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => SIGHASH_DEFAULT + case _ => SIGHASH_ALL + } def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { sign(key, sighash(txOwner, commitmentFormat)) @@ -218,6 +224,10 @@ object Transactions { case TxOwner.Local => SIGHASH_ALL case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } + case SimpleTaprootChannelCommitmentFormat => txOwner match { + case TxOwner.Local => SIGHASH_DEFAULT + case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + } } override def confirmationTarget: ConfirmationTarget.Absolute } @@ -228,7 +238,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY, KotlinUtils.kmp2scala(scriptTree.getRight.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(scriptTree.getRight.hash())) case _: InputInfo.SegwitInput => super.sign(privateKey, txOwner, commitmentFormat) } @@ -236,9 +246,8 @@ object Transactions { override def checkSig(sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = input match { case t: InputInfo.TaprootInput => import KotlinUtils._ - val sighash = this.sighash(txOwner, commitmentFormat) val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt - val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex = 0, Seq(input.txOut), sighash, scriptTree.getRight.hash()) + val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex = 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), scriptTree.getRight.hash()) Crypto.verifySignatureSchnorr(data, sig, pubKey.xOnly) case _: InputInfo.SegwitInput => super.checkSig(sig, pubKey, txOwner, commitmentFormat) @@ -251,7 +260,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY, KotlinUtils.kmp2scala(scriptTree.getLeft.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(scriptTree.getLeft.hash())) case _: InputInfo.SegwitInput => super.sign(privateKey, txOwner, commitmentFormat) } @@ -259,9 +268,8 @@ object Transactions { override def checkSig(sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = input match { case t: InputInfo.TaprootInput => import KotlinUtils._ - val sighash = this.sighash(txOwner, commitmentFormat) val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt - val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex = 0, Seq(input.txOut), sighash, scriptTree.getLeft.hash()) + val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex = 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), scriptTree.getLeft.hash()) Crypto.verifySignatureSchnorr(data, sig, pubKey.xOnly) case _: InputInfo.SegwitInput => super.checkSig(sig, pubKey, txOwner, commitmentFormat) @@ -273,7 +281,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(scriptTree: ScriptTree.Leaf) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(scriptTree.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(scriptTree.hash())) case _: InputInfo.SegwitInput => super.sign(privateKey, txOwner, commitmentFormat) } @@ -290,7 +298,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(htlcTree: ScriptTree.Branch) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(htlcTree.getRight.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(htlcTree.getRight.hash())) case _: InputInfo.SegwitInput => super.sign(privateKey, txOwner, commitmentFormat) } @@ -301,7 +309,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(htlcTree: ScriptTree.Branch) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(htlcTree.getLeft.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(htlcTree.getLeft.hash())) case _: InputInfo.SegwitInput => super.sign(privateKey, txOwner, commitmentFormat) } @@ -312,7 +320,7 @@ object Transactions { override def desc: String = "local-anchor" override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { - case _: InputInfo.TaprootInput => Transaction.signInputTaprootKeyPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree)) + case _: InputInfo.TaprootInput => Transaction.signInputTaprootKeyPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), Some(Scripts.Taproot.anchorScriptTree)) case _: InputInfo.SegwitInput => super.sign(privateKey, txOwner, commitmentFormat) } } @@ -326,7 +334,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(toRemoteScriptTree: ScriptTree.Leaf) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(toRemoteScriptTree.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(toRemoteScriptTree.hash())) case _: InputInfo.SegwitInput => { super.sign(privateKey, txOwner, commitmentFormat) } @@ -339,7 +347,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(toLocalScriptTree: ScriptTree.Branch) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(toLocalScriptTree.getLeft.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(toLocalScriptTree.getLeft.hash())) case _: InputInfo.SegwitInput => { super.sign(privateKey, txOwner, commitmentFormat) } @@ -352,7 +360,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => val Some(toLocalScriptTree: ScriptTree.Branch) = t.scriptTree_opt - Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(toLocalScriptTree.getRight.hash())) + Transaction.signInputTaprootScriptPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), KotlinUtils.kmp2scala(toLocalScriptTree.getRight.hash())) case _: InputInfo.SegwitInput => { super.sign(privateKey, txOwner, commitmentFormat) } @@ -364,7 +372,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => - Transaction.signInputTaprootKeyPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, t.scriptTree_opt) + Transaction.signInputTaprootKeyPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), t.scriptTree_opt) case _: InputInfo.SegwitInput => { super.sign(privateKey, txOwner, commitmentFormat) } @@ -376,7 +384,7 @@ object Transactions { override def sign(privateKey: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = input match { case t: InputInfo.TaprootInput => - Transaction.signInputTaprootKeyPath(privateKey, tx, 0, Seq(input.txOut), SigHash.SIGHASH_DEFAULT, t.scriptTree_opt) + Transaction.signInputTaprootKeyPath(privateKey, tx, 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), t.scriptTree_opt) case _: InputInfo.SegwitInput => { super.sign(privateKey, txOwner, commitmentFormat) } @@ -384,8 +392,6 @@ object Transactions { } 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" } @@ -529,7 +535,7 @@ object Transactions { // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the channel initiator's main output. val anchorsCost = commitmentFormat match { case DefaultCommitmentFormat => Satoshi(0) - case _: AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 } txFee + anchorsCost } @@ -583,7 +589,7 @@ object Transactions { def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match { case DefaultCommitmentFormat => 0 // htlc txs immediately spend the commit tx - case _: AnchorOutputsCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors } /** @@ -645,8 +651,8 @@ object Transactions { val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink[CommitmentOutput]] trimOfferedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - commitmentFormat.useTaproot match { - case true => + commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => val offeredHtlcTree = Scripts.Taproot.offeredHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash) outputs.append(CommitmentOutputLink( TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2tr(localRevocationPubkey.xOnly, Some(offeredHtlcTree))), localRevocationPubkey.xOnly, offeredHtlcTree, OutHtlc(htlc) @@ -658,8 +664,8 @@ object Transactions { } trimReceivedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - commitmentFormat.useTaproot match { - case true => + commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => val receivedHtlcTree = Scripts.Taproot.receivedHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash, htlc.add.cltvExpiry) outputs.append(CommitmentOutputLink( TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2tr(localRevocationPubkey.xOnly, Some(receivedHtlcTree))), localRevocationPubkey.xOnly, receivedHtlcTree, InHtlc(htlc) @@ -679,13 +685,14 @@ object Transactions { } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway if (toLocalAmount >= localDustLimit) { - if (commitmentFormat.useTaproot) { + commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => val toLocalScriptTree = Scripts.Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) outputs.append(CommitmentOutputLink( TxOut(toLocalAmount, pay2tr(XonlyPublicKey(NUMS_POINT), Some(toLocalScriptTree))), NUMS_POINT.xOnly, toLocalScriptTree, ToLocal)) - } else { + case _ => outputs.append(CommitmentOutputLink( TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), @@ -695,7 +702,7 @@ object Transactions { if (toRemoteAmount >= localDustLimit) { commitmentFormat match { - case _ if commitmentFormat.useTaproot => + case SimpleTaprootChannelCommitmentFormat => val toRemoteScriptTree = Scripts.Taproot.toRemoteScriptTree(remotePaymentPubkey) outputs.append(CommitmentOutputLink( TxOut(toRemoteAmount, pay2tr(XonlyPublicKey(NUMS_POINT), Some(toRemoteScriptTree))), @@ -713,7 +720,7 @@ object Transactions { } commitmentFormat match { - case _ if commitmentFormat.useTaproot => + case SimpleTaprootChannelCommitmentFormat => if (toLocalAmount >= localDustLimit || hasHtlcs) { outputs.append( CommitmentOutputLink.TaprootLink( @@ -1456,8 +1463,7 @@ object Transactions { case t: InputInfo.TaprootInput => t.scriptTree_opt match { case Some(htlcTree: ScriptTree.Branch) => - val sigHash = (SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY).toByte - Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig :+ sigHash, localSig :+ sigHash, paymentPreimage)), htlcTree) + Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT), paymentPreimage)), htlcTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc successTx tx") } } @@ -1470,8 +1476,7 @@ object Transactions { case t: InputInfo.TaprootInput => t.scriptTree_opt match { case Some(htlcTree: ScriptTree.Branch) => - val sigHash = (SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY).toByte - Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig :+ sigHash, localSig :+ sigHash)), htlcTree) + Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT))), htlcTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc timeout tx") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 20768d649..a0c99f9cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -1572,7 +1572,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 4) - case _: AnchorOutputsCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 6) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 6) } probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) @@ -1583,7 +1583,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => () - case _: AnchorOutputsCommitmentFormat => alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor } if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index c9c2b4800..0638180c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -590,7 +590,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // all htlcs success/timeout should be published as-is, without claiming their outputs s2blockchain.expectMsgAllOf(localCommitPublished.htlcTxs.values.toSeq.collect { case Some(tx) => TxPublisher.PublishFinalTx(tx, tx.fee, Some(commitTx.txid)) }: _*) assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => // all htlcs success/timeout should be published as replaceable txs, without claiming their outputs val htlcTxs = localCommitPublished.htlcTxs.values.collect { case Some(tx: HtlcTx) => tx } val publishedTxs = htlcTxs.map(_ => s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx]) @@ -629,7 +629,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // If anchor outputs is used, we use the anchor output to bump the fees if necessary. closingData.commitments.params.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => val anchorTx = s2blockchain.expectMsgType[PublishReplaceableTx] assert(anchorTx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) case Transactions.DefaultCommitmentFormat => () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala index ceb7082d2..47d31ea7c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala @@ -55,7 +55,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val aliceListener = TestProbe() val bobListener = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index fd778546c..65229afd7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -61,7 +61,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 7c7bf41f5..21e3f2ffa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -53,7 +53,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) val aliceInit = Init(aliceParams.initFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index d3af777ed..b863cb810 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -72,7 +72,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 52f7f8a04..869f4533b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -990,7 +990,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case _: AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -1124,7 +1124,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -1319,7 +1319,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs } alice ! WatchFundingSpentTriggered(bobCommitTx) @@ -1393,7 +1393,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) // 2 main outputs } @@ -1409,7 +1409,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) } @@ -1425,7 +1425,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) } @@ -1439,7 +1439,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) } @@ -1820,7 +1820,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) val initOutputCount = channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 4 + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 4 case DefaultCommitmentFormat => 2 } assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 8b6c93940..a1adc8c1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -35,7 +35,7 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, TxOwner} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{OutgoingHtlc, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} @@ -181,7 +181,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks(25, Some(minerAddress)) val expectedTxCountC = 1 // C should have 1 recv transaction: its main output val expectedTxCountF = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 2 // F should have 2 recv transactions: the redeemed htlc and its main output + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 2 // F should have 2 recv transactions: the redeemed htlc and its main output case Transactions.DefaultCommitmentFormat => 1 // F's main output uses static_remotekey } awaitCond({ @@ -221,7 +221,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate enough blocks so that F gets its htlc-success delayed output generateBlocks(25, Some(minerAddress)) val expectedTxCountC = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 1 // C should have 1 recv transaction: its main output + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 1 // C should have 1 recv transaction: its main output case Transactions.DefaultCommitmentFormat => 0 // C's main output uses static_remotekey } val expectedTxCountF = 2 // F should have 2 recv transactions: the redeemed htlc and its main output @@ -275,7 +275,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks(25, Some(minerAddress)) val expectedTxCountC = 2 // C should have 2 recv transactions: its main output and the htlc timeout val expectedTxCountF = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 1 // F should have 1 recv transaction: its main output + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 1 // F should have 1 recv transaction: its main output case Transactions.DefaultCommitmentFormat => 0 // F's main output uses static_remotekey } awaitCond({ @@ -330,7 +330,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) val expectedTxCountC = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 2 // C should have 2 recv transactions: its main output and the htlc timeout + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 2 // C should have 2 recv transactions: its main output and the htlc timeout case Transactions.DefaultCommitmentFormat => 1 // C's main output uses static_remotekey } val expectedTxCountF = 1 // F should have 1 recv transaction: its main output @@ -405,7 +405,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val localCommitF = commitmentsF.latest.localCommit commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) } val outgoingHtlcExpiry = localCommitF.spec.htlcs.collect { case OutgoingHtlc(add) => add.cltvExpiry }.max val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 4ae3689ce..2028d1159 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -770,14 +770,15 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, commitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val commitTx = if (commitmentFormat.useTaproot) { + val commitTx = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => val Right(sig) = for { localPartialSig <- Musig2.signTaprootInput(localFundingPriv, txInfo.tx, 0, Seq(fundingOutput), publicKeys, secretLocalNonce, publicNonces, None) remotePartialSig <- Musig2.signTaprootInput(remoteFundingPriv, txInfo.tx, 0, Seq(fundingOutput), publicKeys, secretRemoteNonce, publicNonces, None) sig <- Musig2.aggregateTaprootSignatures(Seq(localPartialSig, remotePartialSig), txInfo.tx, 0, Seq(fundingOutput), publicKeys, publicNonces, None) } yield sig Transactions.addAggregatedSignature(txInfo, sig) - } else { + case _ => val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, commitmentFormat) val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, commitmentFormat) Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) @@ -815,10 +816,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // local spends local anchor - val anchorKey = if (commitmentFormat.useTaproot) { - localDelayedPaymentPriv - } else { - localFundingPriv + val anchorKey = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => localDelayedPaymentPriv + case _ => localFundingPriv } val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, anchorKey.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) @@ -828,10 +828,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends remote anchor - val anchorKey = if (commitmentFormat.useTaproot) { - remotePaymentPriv - } else { - remoteFundingPriv + val anchorKey = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => remotePaymentPriv + case _ => remoteFundingPriv } val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, anchorKey.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) @@ -950,10 +949,11 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Some(htlcOutputIndex) = commitTxOutputs.map(_.filter[OutHtlc]).zipWithIndex.collectFirst { case (Some(co), outputIndex) if co.commitmentOutput.outgoingHtlc.add.id == htlc1.id => outputIndex } - val Right(htlcPenaltyTx) = if (commitmentFormat.useTaproot) { + val Right(htlcPenaltyTx) = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, htlc1.paymentHash) makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, localRevocationPriv.publicKey.xOnly, Some(scriptTree), localDustLimit, finalPubKeyScript, feeratePerKw) - } else { + case _ => val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), commitmentFormat)) makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) } @@ -967,10 +967,11 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Some(htlcOutputIndex) = commitTxOutputs.map(_.filter[InHtlc]).zipWithIndex.collectFirst { case (Some(co), outputIndex) if co.commitmentOutput.incomingHtlc.add.id == htlc.id => outputIndex } - val Right(htlcPenaltyTx) = if (commitmentFormat.useTaproot) { + val Right(htlcPenaltyTx) = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => val scriptTree = Taproot.receivedHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, htlc.paymentHash, htlc.cltvExpiry) makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, localRevocationPriv.publicKey.xOnly, Some(scriptTree), localDustLimit, finalPubKeyScript, feeratePerKw) - } else { + case _ => val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry, commitmentFormat)) makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) } @@ -1120,9 +1121,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { txOut = TxOut(25_000.sat, Taproot.htlcDelayed(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)) :: Nil, lockTime = 300) val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash) - val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY - val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash) - val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash) + val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY, scriptTree.getLeft.hash()), SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY) + val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_DEFAULT, scriptTree.getLeft.hash()), SigHash.SIGHASH_DEFAULT) val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig)), scriptTree) tx.updateWitness(0, witness) }