diff --git a/.mvn/checksums/checksums-central.sha256 b/.mvn/checksums/checksums-central.sha256 index 5408848e87..e02ffb58c1 100644 --- a/.mvn/checksums/checksums-central.sha256 +++ b/.mvn/checksums/checksums-central.sha256 @@ -169,7 +169,6 @@ 224fe4d0c650f085c012f0a03c1995c598c7b5c506bc5350b727c75874330f00 org/codehaus/plexus/plexus-classworlds/1.2-alpha-9/plexus-classworlds-1.2-alpha-9.pom 22e81ee9d5349ba0e897adb5c7fcec25656c9e0b7b86c3e93e9a7af360481c0d org/scala-lang/scala-library/2.12.8/scala-library-2.12.8.pom 2340855d40ce6125d9a23ab80d94848efa50b2957cf93531e2a7dcf631b4f22b org/apache/maven/maven-settings/3.0/maven-settings-3.0.pom -23e38d74de736eaece8a973f3e20e5dec49c92a640bb6156f23cd60199ca4aa2 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.22.2/bitcoin-kmp-jvm-0.22.2.jar 240113a2f22fd1f0b182b32baecf0e7876b3a8e41f3c4da3335eeb9ffb24b9f4 org/sonatype/sisu/sisu-guice/2.1.7/sisu-guice-2.1.7-noaop.jar 2431faf4c35b658b2e98f2ea4e10f5e7bd95d11bbb75338856088fe1099c14fb org/apache/maven/maven-resolver-provider/3.8.6/maven-resolver-provider-3.8.6.pom 243c66f842cd2b3ded7c6d2c36b177a65c3f5d94800cef988ba3e29ec8cf60c9 org/apache/maven/doxia/doxia-logging-api/1.11.1/doxia-logging-api-1.11.1.jar @@ -311,6 +310,7 @@ 3f504cac405ce066d5665ff69541484d5322f35ac7a7ec6104cf86a01008e02d com/fasterxml/jackson/core/jackson-databind/2.12.7.1/jackson-databind-2.12.7.1.jar 3f98f587e527a58e0be4bbe2ea13263a83772029171a0a6d51e8629bad365ff6 com/typesafe/akka/akka-testkit_2.13/2.6.20/akka-testkit_2.13-2.6.20.jar 3f9eab1c0da7246f0add684d37c9bd1de83270735ae09777e95074a54f02d2d5 com/typesafe/akka/akka-testkit_2.13/2.6.20/akka-testkit_2.13-2.6.20.pom +3fdb0e0b37907e1d73a958db91f317bb8a65bfd88b69b9fe5c460cf8d5883ef8 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.23.0/bitcoin-kmp-jvm-0.23.0.pom 40091058e34f3410c38fbec606dc954eadd96dd7d97ca06bfa5abdb84294c043 com/github/oshi/oshi-core/6.4.13/oshi-core-6.4.13.jar 41b7221f1c4f7656be0e5777c6f3df99452ad0b8a54988d45febb368058e259a com/typesafe/akka/akka-remote_2.13/2.6.20/akka-remote_2.13-2.6.20.jar 42d759c550d723373ae34556e80930b9ed2e13495dace134adf99e64ddc8d2e1 org/apache/maven/plugins/maven-plugins/35/maven-plugins-35.pom @@ -329,7 +329,6 @@ 4510588fbad0b6689fbc4d1c0bd91d255c343a607d1e406f502155ec79d5434b fr/acinq/secp256k1/secp256k1-kmp-jni-jvm/0.17.3/secp256k1-kmp-jni-jvm-0.17.3.jar 454381d9535918f78b4024a9655fba4b3e522312bcf78c263cf8c6dda873c604 org/mockito/mockito-scala-scalatest_2.13/1.17.5/mockito-scala-scalatest_2.13-1.17.5.pom 45a8e898eb668337aea6caeee2ca53be0efe9af631554bd69a781542762cb2be io/netty/netty-all/4.1.94.Final/netty-all-4.1.94.Final.pom -45cc11268c1cee5de307595bf2ec2d8a1c17faa794f8e0a8657f74a9f04eb879 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.22.2/bitcoin-kmp-jvm-0.22.2.pom 46300ff8f2885d679df1d0123c4e575a73c8ed1a87a206751e1bffa2b1d61702 fr/acinq/secp256k1/secp256k1-kmp-jni-common/0.17.3/secp256k1-kmp-jni-common-0.17.3.pom 468ddd2df93670b14b2258a3da80a9e2b49205f199d4a6185a12907207114655 org/apache/maven/surefire/surefire-booter/3.1.2/surefire-booter-3.1.2.pom 469a6c59f92effa62c0797ce7d52d2c03cf8ee1034b923c360dd78a9f505a7ba org/codehaus/plexus/plexus-classworlds/2.6.0/plexus-classworlds-2.6.0.pom @@ -610,7 +609,6 @@ 866414588fe0a8fb7341baa987f6fee05671b9859e28c32cb63bc529f42a63a9 com/fasterxml/jackson/core/jackson-core/2.12.7/jackson-core-2.12.7.pom 86c2d5e817489e1b478bd713c5cd8ad980eb9045fa831ef3a0d72952a20d4395 org/scala-lang/scala-compiler/2.13.11/scala-compiler-2.13.11.pom 86e0255d4c879c61b4833ed7f13124e8bb679df47debb127326e7db7dd49a07b org/codehaus/plexus/plexus-utils/3.5.1/plexus-utils-3.5.1.jar -86e74b15b39d9b02e8db92129a1de62ccf040997f0b843a29f52408784a8b1bd fr/acinq/bitcoin-lib_2.13/0.37/bitcoin-lib_2.13-0.37.jar 873139960c4c780176dda580b003a2c4bf82188bdce5bb99234e224ef7acfceb org/codehaus/plexus/plexus-sec-dispatcher/2.0/plexus-sec-dispatcher-2.0.jar 879b3e718453c8b934ff5e8225107a24701bde392f96daf6135f94f9e161dbc5 org/scala-lang/modules/scala-java8-compat_2.13/1.0.0/scala-java8-compat_2.13-1.0.0.jar 87e66ffad03aa18129ea0762d2c02f566a9480e6eee8d84e25e1b931f12ea831 org/eclipse/sisu/org.eclipse.sisu.plexus/0.3.4/org.eclipse.sisu.plexus-0.3.4.jar @@ -629,6 +627,7 @@ 8a8ecb570553bf9f1ffae211a8d4ca9ee630c17afe59293368fba7bd9b42fcb7 org/apache/commons/commons-parent/47/commons-parent-47.pom 8abf8511bb13a26ef1c481ce22b0fba8cf12fa399740e28123c06f70b5007103 com/typesafe/akka/akka-remote_2.13/2.6.20/akka-remote_2.13-2.6.20.pom 8b30025f0ecb40d2b71a71ffeb6e97dfc7c43ce3cf2c698e51c7afac474b10ea org/json4s/json4s-jackson-core_2.13/4.0.6/json4s-jackson-core_2.13-4.0.6.pom +8b43a582c4e91256dae8b09999dc4a69e18bc472c636a717a03d8be44691fa83 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.23.0/bitcoin-kmp-jvm-0.23.0.jar 8c0e6aa7f35593016f2c5e78b604b57f023cdaca3561fe2fe36f2b5dbbae1d16 org/eclipse/sisu/org.eclipse.sisu.inject/0.3.4/org.eclipse.sisu.inject-0.3.4.jar 8c19e7148bee907597129b2fd706839c45db849c72a25285ec1674f0ffdabf8e org/zeromq/jeromq/0.5.2/jeromq-0.5.2.jar 8cbcb2aacd7f4a7759866ce91b2f910310fbe5a586b5fc7b9bdb76af9257e7c4 org/codehaus/plexus/plexus-components/1.3.1/plexus-components-1.3.1.pom @@ -752,6 +751,7 @@ a70e1f662fa81b72eb468d28eec72fd7f2b7b49c4b54d1cf1c14ccd197d4eafd org/apache/mav a75a5241f1a54af90ecfc0eb98a2d653f1b4a3c9e95530a119379863d7c41a92 com/typesafe/akka/akka-http-testkit_2.13/10.2.7/akka-http-testkit_2.13-10.2.7.jar a75afa84ca35a50225991b39e6b6278186e612f7a2a0c0e981de523aaac516a4 io/netty/netty-transport/4.1.94.Final/netty-transport-4.1.94.Final.jar a775e6bbf89895978ea3b702aa759fd42c0f128e63d0a589fd5cf5d8afbf5451 org/slf4j/slf4j-api/2.0.0-alpha1/slf4j-api-2.0.0-alpha1.pom +a79bd4c556004f21b9350242f1838a873ea4d32708ed3bf3f3a24c7e6c3d803d fr/acinq/bitcoin-lib_2.13/0.39/bitcoin-lib_2.13-0.39.jar a7cb7fcc257ae8b3d6089e21c5607c32d284c7955a7a0ac5d351a30298a7ab84 com/typesafe/akka/akka-stream_2.13/2.6.20/akka-stream_2.13-2.6.20.jar a7f1fec73e53a9796bcfd8d41c490d61dd70141604752e6e75b2e755f044fe8f org/scala-sbt/compiler-interface/1.8.0/compiler-interface-1.8.0.jar a837bd7d73291564dc8e8c826de0fede75896527a35bdcddb77b0545ee656a4c org/codehaus/plexus/plexus-archiver/4.9.2/plexus-archiver-4.9.2.jar @@ -797,7 +797,6 @@ b1587c577a8f244aff37bc1418443e01ffbf4291536483de6ecd0054aa460679 com/typesafe/a b163c1cfc8fc1fd58b457a00d586c04c46e986d75904e9ca54c03a97d65b496c org/junit/junit-bom/5.9.1/junit-bom-5.9.1.pom b1a00f5b1c4dbe62b805d65d23911a6f77063889d7cb1e86fe8389d6190473f7 org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.pom b1a163d1c94c0e922f49ce58932e28c12c78b7ea4bb164052694a01b47f9e895 io/netty/netty-codec-haproxy/4.1.94.Final/netty-codec-haproxy-4.1.94.Final.pom -b2176722f4696d151d4bf91860723ca64d10b68154f949ea3b0173f26f1ab330 fr/acinq/bitcoin-lib_2.13/0.37/bitcoin-lib_2.13-0.37.pom b248cb6f390ee8bceb912af3da471146fdf003702a173d750f986b1d4a3362e6 org/scala-lang/scala-compiler/2.13.8/scala-compiler-2.13.8.jar b2b0fc69e22a650c3892f1c366d77076f29575c6738df4c7a70a44844484cdf9 org/apache/apache/27/apache-27.pom b345048b7692204803b49eb11f5203b52e18aa7647f8b77dd63118fd8d5fd2a2 io/netty/netty-codec-dns/4.1.94.Final/netty-codec-dns-4.1.94.Final.jar @@ -993,6 +992,7 @@ e68fc19a48cec582a6732fd0b10dbfe9feca25060963def89e547f8a3759d379 org/apache/apa e6d066a767c5dcaf8b625ed88478b0084883fee256d0e5935b5c896df59f1a91 org/mockito/mockito-scala_2.13/1.17.5/mockito-scala_2.13-1.17.5.pom e6d79207a0b814b5642e26dce24ebc0edaf32a3948fa542ab7097fc44f9592fe io/kamon/kamon-prometheus_2.13/2.7.4/kamon-prometheus_2.13-2.7.4.pom e71e6d9b6a3d559c409030e9ba83c8514cb625b937aeecb900d7a15613622c86 org/json4s/json4s-core_2.13/4.0.3/json4s-core_2.13-4.0.3.jar +e74bae4fe00be1544a2e4b86628a541004a2e9042341d398ceaa7725a3165f39 fr/acinq/bitcoin-lib_2.13/0.39/bitcoin-lib_2.13-0.39.pom e7ebaead3c95e74934451fc5b5ae9d02066303db67430f59fe219714efcf3bf3 com/thoughtworks/qdox/qdox/2.0.3/qdox-2.0.3.pom e855b04820e58822bda1ab448f7b29e2fccf363f1b2ca95c8c05f2d625b28928 org/sonatype/aether/aether-api/1.7/aether-api-1.7.pom e87ea4823ecf2dd856901da359270be904236be59c27e2781eb8d78c97e45b2a org/ow2/asm/asm-commons/5.0.3/asm-commons-5.0.3.pom 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 ed275fc070..5b7850222b 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 a7514601ac..8edcb9b868 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 @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} // @formatter:off sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] { @@ -76,8 +76,8 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax def isProposedFeerateTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = { commitmentFormat match { - case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate + case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate } } @@ -85,7 +85,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax commitmentFormat match { case Transactions.DefaultCommitmentFormat => proposedFeerate < networkFeerate * ratioLow // When using anchor outputs, we allow low feerates: fees will be set with CPFP and RBF at broadcast time. - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat => false + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false } } } @@ -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/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 5a62648009..514795dcb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -196,12 +196,14 @@ object LocalCommit { val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(params, commitKeys, localCommitIndex, fundingKey, remoteFundingPubKey, commitInput, spec) val remoteCommitSigOk = params.commitmentFormat match { case _: SegwitV0CommitmentFormat => localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(commit.signature)) + case _: SimpleTaprootChannelCommitmentFormat => ??? } if (!remoteCommitSigOk) { return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, localCommitIndex, localCommitTx.tx)) } val commitxTxAndRemoteSig = params.commitmentFormat match { case _: SegwitV0CommitmentFormat => CommitTxAndRemoteSig(localCommitTx, ChannelSpendSignature.IndividualSignature(commit.signature)) + case _: SimpleTaprootChannelCommitmentFormat => ??? } val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) if (commit.htlcSignatures.size != sortedHtlcTxs.size) { @@ -230,6 +232,7 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePer case _: SegwitV0CommitmentFormat => val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig CommitSig(params.channelId, sig, htlcSigs.toList) + case _: SimpleTaprootChannelCommitmentFormat => ??? } } } @@ -661,6 +664,7 @@ case class Commitment(fundingTxIndex: Long, case _: SegwitV0CommitmentFormat => val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(tlvs)) + case _: SimpleTaprootChannelCommitmentFormat => ??? } val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) (copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig) @@ -1156,11 +1160,7 @@ case class Commitments(params: ChannelParams, active.forall { commitment => val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey val remoteFundingKey = commitment.remoteFundingPubKey - val redeemInfo = params.commitmentFormat match { - case _: SegwitV0CommitmentFormat => - val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) - RedeemInfo.P2wsh(fundingScript) - } + val redeemInfo = Helpers.Funding.makeFundingScript(localFundingKey, remoteFundingKey, params.commitmentFormat) commitment.commitInput.txOut.publicKeyScript == redeemInfo.pubkeyScript } } 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 4bd2fc1765..978bf8b89e 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 @@ -373,6 +373,7 @@ object Helpers { def makeFundingScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo = { commitmentFormat match { case _: SegwitV0CommitmentFormat => RedeemInfo.P2wsh(Script.write(multiSig2of2(localFundingKey, remoteFundingKey))) + case _: SimpleTaprootChannelCommitmentFormat => RedeemInfo.TaprootKeyPath(Taproot.musig2Aggregate(localFundingKey, remoteFundingKey), None) } } @@ -676,7 +677,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. @@ -901,7 +902,7 @@ object Helpers { def claimAnchor(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): Option[ClaimAnchorOutputTx] = { withTxGenerationLog("local-anchor") { - ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys.publicKeys, commitTx, commitmentFormat) + ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys, commitTx, commitmentFormat) } } @@ -1003,7 +1004,7 @@ object Helpers { def claimAnchor(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): Option[ClaimAnchorOutputTx] = { withTxGenerationLog("remote-anchor") { - ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys.publicKeys, commitTx, commitmentFormat) + ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys, commitTx, commitmentFormat) } } @@ -1014,7 +1015,7 @@ object Helpers { case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { ClaimP2WPKHOutputTx.createSignedTx(commitKeys, commitTx, params.localParams.dustLimit, finalScriptPubKey, feerate, params.commitmentFormat) } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { ClaimRemoteDelayedOutputTx.createSignedTx(commitKeys, commitTx, params.localParams.dustLimit, finalScriptPubKey, feerate, params.commitmentFormat) } } @@ -1132,7 +1133,7 @@ object Helpers { case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { ClaimP2WPKHOutputTx.createSignedTx(commitKeys, commitTx, localParams.dustLimit, finalScriptPubKey, feerateMain, commitmentFormat) } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { ClaimRemoteDelayedOutputTx.createSignedTx(commitKeys, commitTx, localParams.dustLimit, finalScriptPubKey, feerateMain, commitmentFormat) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 9dd4f134d0..ef3895dca0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.io.Peer.OpenChannelResponse import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.transactions.Transactions.SegwitV0CommitmentFormat +import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} import fr.acinq.eclair.{MilliSatoshiLong, UInt64, randomKey, toLongId} import scodec.bits.ByteVector @@ -218,6 +218,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") val localSigOfRemoteTx = params.commitmentFormat match { case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig + case _: SimpleTaprootChannelCommitmentFormat => ??? } // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( @@ -273,6 +274,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case true => val localSigOfRemoteTx = params.commitmentFormat match { case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig + case _: SimpleTaprootChannelCommitmentFormat => ??? } val channelId = toLongId(fundingTxId, fundingTxOutputIndex) val fundingSigned = FundingSigned( 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 2fa5760460..5a761edeb1 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 @@ -277,7 +277,7 @@ trait ErrorHandlers extends CommonHandlers { case _ => None } signedTx_opt.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => val confirmationTarget = ConfirmationTarget.Absolute(htlcTx.htlcExpiry.blockHeight) val replaceableTx_opt = (htlcTx, preimage_opt) match { case (htlcTx: HtlcSuccessTx, Some(preimage)) => Some(ReplaceableHtlcSuccess(htlcTx, commitKeys, preimage, remoteSig, commitTx, commitment)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 1a7606e6be..8b18e06b6a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} -import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, SegwitV0CommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64} @@ -859,6 +859,8 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) + case _: SimpleTaprootChannelCommitmentFormat => ??? + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala index 361d7af4de..ff7ea3da80 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala @@ -104,7 +104,7 @@ sealed trait ReplaceableAnchor extends ReplaceableTxWithWalletInputs { } case class ReplaceableLocalCommitAnchor(txInfo: ClaimAnchorOutputTx, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitment: FullCommitment) extends ReplaceableAnchor { - override def redeemInfo(): RedeemInfo = ClaimAnchorOutputTx.redeemInfo(fundingKey, commitKeys.publicKeys, commitment.params.commitmentFormat) + override def redeemInfo(): RedeemInfo = ClaimAnchorOutputTx.redeemInfo(fundingKey.publicKey, commitKeys, commitment.params.commitmentFormat) override def sign(extraUtxos: Map[OutPoint, TxOut]): ReplaceableLocalCommitAnchor = { copy(txInfo = txInfo.sign(fundingKey, commitKeys, commitment.params.commitmentFormat, extraUtxos)) @@ -112,7 +112,7 @@ case class ReplaceableLocalCommitAnchor(txInfo: ClaimAnchorOutputTx, fundingKey: } case class ReplaceableRemoteCommitAnchor(txInfo: ClaimAnchorOutputTx, fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitment: FullCommitment) extends ReplaceableAnchor { - override def redeemInfo(): RedeemInfo = ClaimAnchorOutputTx.redeemInfo(fundingKey, commitKeys.publicKeys, commitment.params.commitmentFormat) + override def redeemInfo(): RedeemInfo = ClaimAnchorOutputTx.redeemInfo(fundingKey.publicKey, commitKeys, commitment.params.commitmentFormat) override def sign(extraUtxos: Map[OutPoint, TxOut]): ReplaceableRemoteCommitAnchor = { copy(txInfo = txInfo.sign(fundingKey, commitKeys, commitment.params.commitmentFormat, extraUtxos)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala index 80b0cc5de3..564a425592 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.Features import fr.acinq.eclair.channel.ChannelParams -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} /** * Created by t-bast on 10/04/2025. @@ -74,7 +74,7 @@ object LocalCommitmentKeys { theirPaymentPublicKey = params.commitmentFormat match { case DefaultCommitmentFormat if params.channelFeatures.hasFeature(Features.StaticRemoteKey) => params.remoteParams.paymentBasepoint case DefaultCommitmentFormat => ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.paymentBasepoint, localPerCommitmentPoint) - case _: AnchorOutputsCommitmentFormat => params.remoteParams.paymentBasepoint + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => params.remoteParams.paymentBasepoint }, ourPaymentBasePoint = params.localParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint), ourHtlcKey = channelKeys.htlcKey(localPerCommitmentPoint), @@ -123,7 +123,7 @@ object RemoteCommitmentKeys { case None => params.commitmentFormat match { // Note that if we're using option_static_remotekey, a walletStaticPaymentBasepoint will be provided. case DefaultCommitmentFormat => Right(channelKeys.paymentKey(remotePerCommitmentPoint)) - case _: AnchorOutputsCommitmentFormat => Right(channelKeys.paymentBaseSecret) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Right(channelKeys.paymentBaseSecret) } }, theirDelayedPaymentPublicKey = ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 208723d470..f414d8d4ef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.scalacompat.{LexicographicalOrdering, SatoshiLong, TxOut} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol._ /** @@ -93,7 +93,7 @@ case class OutgoingHtlc(add: UpdateAddHtlc) extends DirectedHtlc final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: FeeratePerKw, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match { - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => FeeratePerKw(0 sat) + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => FeeratePerKw(0 sat) case _ => commitTxFeerate } 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 ae9c258eae..6668aeffed 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 @@ -17,14 +17,15 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Script.LOCKTIME_THRESHOLD -import fr.acinq.bitcoin.ScriptTree +import fr.acinq.bitcoin.{ScriptTree, SigHash} import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_MASK, SEQUENCE_LOCKTIME_TYPE_FLAG} +import fr.acinq.bitcoin.io.Output import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, RemoteCommitmentKeys} -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector @@ -46,7 +47,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. */ @@ -207,7 +208,7 @@ object Scripts { def htlcOffered(keys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false - case _: AnchorOutputsCommitmentFormat => true + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key @@ -264,7 +265,7 @@ object Scripts { def htlcReceived(keys: CommitmentPublicKeys, paymentHash: ByteVector32, lockTime: CltvExpiry, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false - case _: AnchorOutputsCommitmentFormat => true + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key @@ -378,11 +379,15 @@ object Scripts { OP_PUSHDATA(keys.localDelayedPaymentPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: Scripts.encodeNumber(toSelfDelay.toInt) :: OP_CHECKSEQUENCEVERIFY :: Nil } + case class ToLocalScriptTree(localDelayed: ScriptTree.Leaf, revocation: ScriptTree.Leaf) { + val scriptTree: ScriptTree.Branch = new ScriptTree.Branch(localDelayed, revocation) + } + /** * @return a script tree with two leaves (to self with delay, and to revocation key) */ - def toLocalScriptTree(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): ScriptTree.Branch = { - new ScriptTree.Branch( + def toLocalScriptTree(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): ToLocalScriptTree = { + ToLocalScriptTree( new ScriptTree.Leaf(toLocalDelayed(keys, toSelfDelay)), new ScriptTree.Leaf(toRevocationKey(keys)), ) @@ -392,7 +397,7 @@ object Scripts { * Script used for the main balance of the owner of the commitment transaction. */ def toLocal(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = { - Script.pay2tr(NUMS_POINT.xOnly, Some(toLocalScriptTree(keys, toSelfDelay))) + Script.pay2tr(NUMS_POINT.xOnly, Some(toLocalScriptTree(keys, toSelfDelay).scriptTree)) } /** @@ -452,23 +457,28 @@ object Scripts { // @formatter:on } + case class OfferedHtlcScriptTree(timeout: ScriptTree.Leaf, success: ScriptTree.Leaf) { + val scriptTree: ScriptTree.Branch = new ScriptTree.Branch(timeout, success) + + def witnessTimeout(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, timeout, ScriptWitness(Seq(Taproot.encodeSig(remoteSig, htlcRemoteSighash(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)), localSig)), scriptTree) + } + + def witnessSuccess(commitKeys: RemoteCommitmentKeys, localSig: ByteVector64, paymentPreimage: ByteVector32): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, success, ScriptWitness(Seq(localSig, paymentPreimage)), scriptTree) + } + } + /** * Script tree used for offered HTLCs. */ - def offeredHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32): ScriptTree.Branch = { - new ScriptTree.Branch( + def offeredHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32): OfferedHtlcScriptTree = { + OfferedHtlcScriptTree( new ScriptTree.Leaf(offeredHtlcTimeout(keys)), new ScriptTree.Leaf(offeredHtlcSuccess(keys, paymentHash)), ) } - /** - * Script used for offered HTLCs. - */ - def offeredHtlc(keys: CommitmentPublicKeys, paymentHash: ByteVector32): Seq[ScriptElt] = { - Script.pay2tr(keys.revocationPublicKey.xOnly, Some(offeredHtlcScriptTree(keys, paymentHash))) - } - /** * Script that can be spent when a received (incoming) HTLC times out. * It is spent using a signature from the receiving node after an absolute delay and a 1-block relative delay. @@ -498,23 +508,28 @@ object Scripts { // @formatter:on } + case class ReceivedHtlcScriptTree(timeout: ScriptTree.Leaf, success: ScriptTree.Leaf) { + val scriptTree = new ScriptTree.Branch(timeout, success) + + def witnessSuccess(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, success, ScriptWitness(Seq(Taproot.encodeSig(remoteSig, htlcRemoteSighash(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)), localSig, paymentPreimage)), scriptTree) + } + + def witnessTimeout(commitKeys: RemoteCommitmentKeys, localSig: ByteVector64): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, timeout, ScriptWitness(Seq(localSig)), scriptTree) + } + } + /** * Script tree used for received HTLCs. */ - def receivedHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32, expiry: CltvExpiry): ScriptTree.Branch = { - new ScriptTree.Branch( + def receivedHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32, expiry: CltvExpiry): ReceivedHtlcScriptTree = { + ReceivedHtlcScriptTree( new ScriptTree.Leaf(receivedHtlcTimeout(keys, expiry)), new ScriptTree.Leaf(receivedHtlcSuccess(keys, paymentHash)), ) } - /** - * Script used for received HTLCs. - */ - def receivedHtlc(keys: CommitmentPublicKeys, paymentHash: ByteVector32, expiry: CltvExpiry): Seq[ScriptElt] = { - Script.pay2tr(keys.revocationPublicKey.xOnly, Some(receivedHtlcScriptTree(keys, paymentHash, expiry))) - } - /** * Script tree used for the output of pre-signed HTLC 2nd-stage transactions. */ 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 0b5197fe92..103b7628ee 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 @@ -18,8 +18,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.SigVersion._ +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey} -import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.bitcoin.{ScriptFlags, ScriptTree} import fr.acinq.eclair._ @@ -28,6 +29,7 @@ import fr.acinq.eclair.channel.ChannelSpendSignature import fr.acinq.eclair.channel.ChannelSpendSignature._ import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.transactions.CommitmentOutput._ +import fr.acinq.eclair.transactions.Scripts.Taproot.NUMS_POINT import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import scodec.bits.ByteVector @@ -148,6 +150,33 @@ object Transactions { */ case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat + sealed trait TaprootCommitmentFormat extends CommitmentFormat + + sealed trait SimpleTaprootChannelCommitmentFormat extends TaprootCommitmentFormat { + // weights for taproot transactions are deterministic since signatures are encoded as 64 bytes and + // not in variable length DER format (around 72 bytes) + override val commitWeight = 960 + override val htlcOutputWeight = 172 + override val htlcTimeoutWeight = 645 + override val htlcSuccessWeight = 705 + override val htlcTimeoutInputWeight = 431 + override val htlcSuccessInputWeight = 491 + override val claimHtlcSuccessWeight = 559 + override val claimHtlcTimeoutWeight = 504 + override val anchorInputWeight = 230 + override val toLocalDelayedWeight = 501 + override val toRemoteWeight = 467 + override val htlcDelayedWeight = 469 + override val mainPenaltyWeight = 531 + override val htlcOfferedPenaltyWeight = 396 + override val htlcReceivedPenaltyWeight = 396 + override val claimHtlcPenaltyWeight = 396 + } + + case object LegacySimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat + + case object ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat + // TODO: we're currently keeping the now unused redeemScript to avoid a painful codec update. When creating v5 codecs // (for taproot channels), don't forget to remove this field from the InputInfo class! case class InputInfo(outPoint: OutPoint, txOut: TxOut, unusedRedeemScript: ByteVector) @@ -168,6 +197,9 @@ object Transactions { case class P2wsh(redeemScript: ByteVector) extends SegwitV0 { override val pubkeyScript: ByteVector = Script.write(Script.pay2wsh(redeemScript)) } + object P2wsh { + def apply(script: Seq[ScriptElt]): P2wsh = P2wsh(Script.write(script)) + } sealed trait Taproot extends RedeemInfo /** @@ -183,8 +215,8 @@ object Transactions { * @param leafHash hash of the leaf script we're spending (must belong to the tree). */ case class TaprootScriptPath(internalKey: XonlyPublicKey, scriptTree: ScriptTree, leafHash: ByteVector32) extends Taproot { - require(Option(scriptTree.findScript(KotlinUtils.scala2kmp(leafHash))).nonEmpty, "script tree must contain the provided leaf") - val redeemScript: ByteVector = KotlinUtils.kmp2scala(scriptTree.findScript(KotlinUtils.scala2kmp(leafHash)).getScript) + val leaf: ScriptTree.Leaf = Option(scriptTree.findScript(leafHash)).getOrElse(throw new IllegalArgumentException("script tree must contain the provided leaf")) + val redeemScript: ByteVector = leaf.getScript override val pubkeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, Some(scriptTree))) } } @@ -199,6 +231,8 @@ object Transactions { } // @formatter:on + case class LocalNonce(secretNonce: SecretNonce, publicNonce: IndividualNonce) + sealed trait TransactionWithInputInfo { // @formatter:off def input: InputInfo @@ -209,23 +243,28 @@ object Transactions { def inputIndex: Int = tx.txIn.indexWhere(_.outPoint == input.outPoint) // @formatter:on - protected def sign(key: PrivateKey, sighash: Int, redeemInfo: RedeemInfo, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { + protected def buildSpentOutputs(extraUtxos: Map[OutPoint, TxOut]): Seq[TxOut] = { + // Callers don't except this function to throw. + // But we want to ensure that we're correctly providing input details, otherwise our signature will silently be + // invalid when using taproot. We verify this in all cases, even when using segwit v0, to ensure that we have as + // many tests as possible that exercise this codepath. val inputsMap = extraUtxos + (input.outPoint -> input.txOut) - tx.txIn.foreach(txIn => { - // Note that using a require here is dangerous, because callers don't except this function to throw. - // But we want to ensure that we're correctly providing input details, otherwise our signature will silently be - // invalid when using taproot. We verify this in all cases, even when using segwit v0, to ensure that we have as - // many tests as possible that exercise this codepath. - require(inputsMap.contains(txIn.outPoint), s"cannot sign $desc with txId=${tx.txid}: missing input details for ${txIn.outPoint}") - }) + tx.txIn.foreach(txIn => require(inputsMap.contains(txIn.outPoint), s"cannot sign $desc with txId=${tx.txid}: missing input details for ${txIn.outPoint}")) + tx.txIn.map(txIn => inputsMap(txIn.outPoint)) + } + + protected def sign(key: PrivateKey, sighash: Int, redeemInfo: RedeemInfo, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { + val spentOutputs = buildSpentOutputs(extraUtxos) // NB: the tx may have multiple inputs, we will only sign the one provided in our input. Bear in mind that the // signature will be invalidated if other inputs are added *afterwards* and sighash was SIGHASH_ALL. redeemInfo match { case redeemInfo: RedeemInfo.SegwitV0 => val sigDER = Transaction.signInput(tx, inputIndex, redeemInfo.redeemScript, sighash, input.txOut.amount, SIGVERSION_WITNESS_V0, key) Crypto.der2compact(sigDER) - case _: RedeemInfo.TaprootKeyPath => ??? - case _: RedeemInfo.TaprootScriptPath => ??? + case t: RedeemInfo.TaprootKeyPath => + Transaction.signInputTaprootKeyPath(key, tx, inputIndex, spentOutputs, sighash, t.scriptTree_opt) + case s: RedeemInfo.TaprootScriptPath => + Transaction.signInputTaprootScriptPath(key, tx, inputIndex, spentOutputs, sighash, s.leafHash) } } @@ -235,8 +274,12 @@ object Transactions { case redeemInfo: RedeemInfo.SegwitV0 => val data = Transaction.hashForSigning(tx, inputIndex, redeemInfo.redeemScript, sighash, input.txOut.amount, SIGVERSION_WITNESS_V0) Crypto.verifySignature(data, sig, publicKey) - case _: RedeemInfo.TaprootKeyPath => ??? - case _: RedeemInfo.TaprootScriptPath => ??? + case _: RedeemInfo.TaprootKeyPath => + val data = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, Seq(input.txOut), sighash) + Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly) + case s: RedeemInfo.TaprootScriptPath => + val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, Seq(input.txOut), sighash, s.leafHash) + Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly) } } else { false @@ -264,14 +307,22 @@ object Transactions { ChannelSpendSignature.IndividualSignature(sig) } + /** Create a partial transaction for the channel's musig2 funding output when using a [[TaprootCommitmentFormat]]. */ + def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, extraUtxos: Map[OutPoint, TxOut], localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = { + val spentOutputs = buildSpentOutputs(extraUtxos) + for { + partialSig <- Musig2.signTaprootInput(localFundingKey, tx, inputIndex, spentOutputs, Scripts.sort(Seq(localFundingKey.publicKey, remoteFundingPubkey)), localNonce.secretNonce, publicNonces, None) + } yield ChannelSpendSignature.PartialSignatureWithNonce(partialSig, localNonce.publicNonce) + } + /** Verify a signature received from the remote channel participant. */ - def checkRemoteSig(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, remoteSig: ChannelSpendSignature): Boolean = { - remoteSig match { - case IndividualSignature(remoteSig) => - val redeemScript = Script.write(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey)) - checkSig(remoteSig, remoteFundingPubkey, SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript)) - case PartialSignatureWithNonce(_, _) => ??? - } + def checkRemoteSig(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, remoteSig: ChannelSpendSignature.IndividualSignature): Boolean = { + val redeemScript = Script.write(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey)) + checkSig(remoteSig.sig, remoteFundingPubkey, SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript)) + } + + def checkRemotePartialSignature(localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, remoteSig: PartialSignatureWithNonce, localNonce: IndividualNonce): Boolean = { + Musig2.verifyTaprootSignature(remoteSig.partialSig, remoteSig.nonce, remoteFundingPubKey, tx, inputIndex, Seq(input.txOut), Scripts.sort(Seq(localFundingPubKey, remoteFundingPubKey)), Seq(localNonce, remoteSig.nonce), scriptTree_opt = None) } /** Aggregate local and remote channel spending signatures for a [[SegwitV0CommitmentFormat]]. */ @@ -279,6 +330,15 @@ object Transactions { val witness = Scripts.witness2of2(localSig.sig, remoteSig.sig, localFundingPubkey, remoteFundingPubkey) tx.updateWitness(inputIndex, witness) } + + /** Aggregate local and remote channel spending partial signatures for a [[TaprootCommitmentFormat]]. */ + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce, extraUtxos: Map[OutPoint, TxOut]): Either[Throwable, Transaction] = { + val spentOutputs = buildSpentOutputs(extraUtxos) + for { + aggregatedSignature <- Musig2.aggregateTaprootSignatures(Seq(localSig.partialSig, remoteSig.partialSig), tx, inputIndex, spentOutputs, sort(Seq(localFundingPubkey, remoteFundingPubkey)), Seq(localSig.nonce, remoteSig.nonce), None) + witness = Script.witnessKeyPathPay2tr(aggregatedSignature) + } yield tx.updateWitness(inputIndex, witness) + } } /** This transaction collaboratively spends the channel funding output to change its capacity. */ @@ -358,6 +418,7 @@ object Transactions { /** Sighash flags to use when signing the transaction. */ def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = commitmentFormat match { case _: SegwitV0CommitmentFormat => SIGHASH_ALL + case _: SimpleTaprootChannelCommitmentFormat => SIGHASH_DEFAULT } } @@ -386,6 +447,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 + } } /** Create redeem information for this HTLC transaction, based on the commitment format used. */ @@ -415,18 +480,19 @@ object Transactions { /** This transaction spends a received (incoming) HTLC from a local or remote commitment by revealing the payment preimage. */ case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry) extends HtlcTx { + override val desc: String = "htlc-success" - override def redeemInfo(commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { - case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => - val redeemScript = Script.write(htlcReceived(commitKeys, paymentHash, htlcExpiry, commitmentFormat)) - RedeemInfo.P2wsh(redeemScript) - } + override def redeemInfo(commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = + HtlcSuccessTx.redeemInfo(commitKeys, paymentHash, htlcExpiry, commitmentFormat) def addSigs(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = { val witness = redeemInfo(commitKeys.publicKeys, commitmentFormat) match { - case redeemInfo: RedeemInfo.SegwitV0 => witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, redeemInfo.redeemScript, commitmentFormat) - case _: RedeemInfo.Taproot => ??? + case redeemInfo: RedeemInfo.SegwitV0 => + witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, redeemInfo.redeemScript, commitmentFormat) + case _: RedeemInfo.Taproot => + val receivedHtlcTree = Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry) + receivedHtlcTree.witnessSuccess(commitKeys, localSig, remoteSig, paymentPreimage) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -447,22 +513,33 @@ object Transactions { ) HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry) } + + def redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(htlcReceived(commitKeys, paymentHash, htlcExpiry, commitmentFormat)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val receivedHtlcTree = Taproot.receivedHtlcScriptTree(commitKeys, paymentHash, htlcExpiry) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, receivedHtlcTree.scriptTree, receivedHtlcTree.success.hash()) + } + } /** This transaction spends an offered (outgoing) HTLC from a local or remote commitment after its expiry. */ case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry) extends HtlcTx { + override val desc: String = "htlc-timeout" - override def redeemInfo(commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { - case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => - val redeemScript = Script.write(htlcOffered(commitKeys, paymentHash, commitmentFormat)) - RedeemInfo.P2wsh(redeemScript) - } + override def redeemInfo(commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = + HtlcTimeoutTx.redeemInfo(commitKeys, paymentHash, commitmentFormat) def addSigs(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = { val witness = redeemInfo(commitKeys.publicKeys, commitmentFormat) match { - case redeemInfo: RedeemInfo.SegwitV0 => witnessHtlcTimeout(localSig, remoteSig, redeemInfo.redeemScript, commitmentFormat) - case _: RedeemInfo.Taproot => ??? + case redeemInfo: RedeemInfo.SegwitV0 => + witnessHtlcTimeout(localSig, remoteSig, redeemInfo.redeemScript, commitmentFormat) + case _: RedeemInfo.Taproot => + val offeredHtlcTree = Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash) + offeredHtlcTree.witnessTimeout(commitKeys, localSig, remoteSig) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -483,6 +560,15 @@ object Transactions { ) HtlcTimeoutTx(input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry) } + + def redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(htlcOffered(commitKeys, paymentHash, commitmentFormat)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val offeredHtlcTree = Taproot.offeredHtlcScriptTree(commitKeys, paymentHash) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, offeredHtlcTree.scriptTree, offeredHtlcTree.timeout.hash()) + } } /** This transaction spends the output of a local [[HtlcTx]] after a to_self_delay relative delay. */ @@ -495,6 +581,11 @@ object Transactions { val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) val sig = sign(commitKeys.ourDelayedPaymentKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) witnessToLocalDelayedAfterDelay(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toLocalDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, scriptTree, scriptTree.hash()) + val sig = sign(commitKeys.ourDelayedPaymentKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, scriptTree, ScriptWitness(Seq(sig)), scriptTree) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -502,12 +593,8 @@ object Transactions { object HtlcDelayedTx { def createSignedTx(commitKeys: LocalCommitmentKeys, htlcTx: Transaction, localDustLimit: Satoshi, toLocalDelay: CltvExpiryDelta, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcDelayedTx] = { - val redeemInfo = commitmentFormat match { - case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => - val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) - RedeemInfo.P2wsh(redeemScript) - } - findPubKeyScriptIndex(htlcTx, redeemInfo.pubkeyScript) match { + val pubkeyScript = redeemInfo(commitKeys.publicKeys, toLocalDelay, commitmentFormat).pubkeyScript + findPubKeyScriptIndex(htlcTx, pubkeyScript) match { case Left(skip) => Left(skip) case Right(outputIndex) => val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), ByteVector.empty) @@ -522,6 +609,15 @@ object Transactions { skipTxIfBelowDust(unsignedTx, localDustLimit, () => unsignedTx.sign(commitKeys, commitmentFormat)) } } + + def redeemInfo(commitKeys: CommitmentPublicKeys, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys, toLocalDelay)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.htlcDelayedScriptTree(commitKeys, toLocalDelay) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, scriptTree, scriptTree.hash()) + } } sealed trait ClaimHtlcTx extends ForceCloseTransaction { @@ -543,6 +639,11 @@ object Transactions { val redeemScript = Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat)) val sig = sign(commitKeys.ourHtlcKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) witnessClaimHtlcSuccessFromCommitTx(sig, paymentPreimage, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val offeredTree = Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, offeredTree.scriptTree, offeredTree.success.hash()) + val sig = sign(commitKeys.ourHtlcKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + offeredTree.witnessSuccess(commitKeys, sig, paymentPreimage) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -597,6 +698,11 @@ object Transactions { val redeemScript = Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat)) val sig = sign(commitKeys.ourHtlcKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) witnessClaimHtlcTimeoutFromCommitTx(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val offeredTree = Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, offeredTree.scriptTree, offeredTree.timeout.hash()) + val sig = sign(commitKeys.ourHtlcKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + offeredTree.witnessTimeout(commitKeys, sig) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -646,12 +752,16 @@ object Transactions { def sign(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ClaimAnchorOutputTx = { commitmentFormat match { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => sign(fundingKey, commitmentFormat, extraUtxos) + case _: SimpleTaprootChannelCommitmentFormat => sign(commitKeys.ourDelayedPaymentKey, commitmentFormat, extraUtxos) } } def sign(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ClaimAnchorOutputTx = { commitmentFormat match { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => sign(fundingKey, commitmentFormat, extraUtxos) + case _: SimpleTaprootChannelCommitmentFormat => + val Right(ourPaymentKey) = commitKeys.ourPaymentKey + sign(ourPaymentKey, commitmentFormat, extraUtxos) } } @@ -661,22 +771,43 @@ object Transactions { val redeemScript = Script.write(anchor(anchorKey.publicKey)) val sig = sign(anchorKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos) witnessAnchor(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val redeemInfo = RedeemInfo.TaprootKeyPath(anchorKey.xOnlyPublicKey(), Some(Taproot.anchorScriptTree)) + val sig = sign(anchorKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos) + Script.witnessKeyPathPay2tr(sig) } copy(tx = tx.updateWitness(inputIndex, witness)) } } object ClaimAnchorOutputTx { - def redeemInfo(fundingKey: PrivateKey, commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = { + def redeemInfo(fundingKey: PublicKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat): RedeemInfo = + redeemInfo(fundingKey, commitKeys.publicKeys, toLocal = true, commitmentFormat) + + def redeemInfo(fundingKey: PublicKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat): RedeemInfo = + redeemInfo(fundingKey, commitKeys.publicKeys, toLocal = false, commitmentFormat) + + /** + * + * @param fundingKey funding public keys + * @param commitKeys commitment keys + * @param toLocal true if this is the redeem info for the `toLocal` commit tx output + * @param commitmentFormat commitment format + * @return the redeem information for a local or remote commit tx anchor output + */ + def redeemInfo(fundingKey: PublicKey, commitKeys: CommitmentPublicKeys, toLocal: Boolean, commitmentFormat: CommitmentFormat): RedeemInfo = { commitmentFormat match { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => - val redeemScript = Script.write(anchor(fundingKey.publicKey)) + val redeemScript = Script.write(anchor(fundingKey)) RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val anchorKey = if (toLocal) commitKeys.localDelayedPaymentPublicKey.xOnly else commitKeys.remotePaymentPublicKey.xOnly + RedeemInfo.TaprootKeyPath(anchorKey, Some(Taproot.anchorScriptTree)) } } - def createUnsignedTx(fundingKey: PrivateKey, commitKeys: CommitmentPublicKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { - val pubkeyScript = redeemInfo(fundingKey, commitKeys, commitmentFormat).pubkeyScript + private def createUnsignedTx(redeemInfo: RedeemInfo, commitTx: Transaction): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { + val pubkeyScript = redeemInfo.pubkeyScript findPubKeyScriptIndex(commitTx, pubkeyScript).map { outputIndex => val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty) val unsignedTx = Transaction( @@ -688,6 +819,14 @@ object Transactions { ClaimAnchorOutputTx(input, unsignedTx) } } + + def createUnsignedTx(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { + createUnsignedTx(redeemInfo(fundingKey.publicKey, commitKeys, commitmentFormat), commitTx) + } + + def createUnsignedTx(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { + createUnsignedTx(redeemInfo(fundingKey.publicKey, commitKeys, commitmentFormat), commitTx) + } } sealed trait ClaimRemoteCommitMainOutputTx extends ForceCloseTransaction @@ -741,6 +880,11 @@ object Transactions { val redeemScript = Script.write(toRemoteDelayed(commitKeys.publicKeys)) val sig = sign(priv, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) witnessClaimToRemoteDelayedFromCommitTx(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.toRemoteScriptTree(commitKeys.publicKeys) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scriptTree, scriptTree.hash()) + val sig = sign(priv, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, scriptTree, ScriptWitness(Seq(sig)), scriptTree) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -753,6 +897,9 @@ object Transactions { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => val redeemScript = Script.write(toRemoteDelayed(commitKeys.publicKeys)) RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.toRemoteScriptTree(commitKeys.publicKeys) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scriptTree, scriptTree.hash()) } findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match { case Left(skip) => Left(skip) @@ -781,6 +928,11 @@ object Transactions { val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) val sig = sign(commitKeys.ourDelayedPaymentKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) witnessToLocalDelayedAfterDelay(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toLocalDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + val sig = sign(commitKeys.ourDelayedPaymentKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(Seq(sig)), toLocalTree.scriptTree) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -792,6 +944,9 @@ object Transactions { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toLocalDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) } findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match { case Left(skip) => Left(skip) @@ -820,6 +975,11 @@ object Transactions { val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) val sig = sign(revocationKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toRemoteDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.revocation.hash()) + val sig = sign(revocationKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(Seq(sig)), toLocalTree.scriptTree) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -831,6 +991,9 @@ object Transactions { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toRemoteDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.revocation.hash()) } findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match { case Left(skip) => Left(skip) @@ -860,8 +1023,8 @@ object Transactions { val witness = redeemInfo match { case RedeemInfo.P2wpkh(_) => Script.witnessPay2wpkh(revocationKey.publicKey, der(sig)) case RedeemInfo.P2wsh(redeemScript) => Scripts.witnessHtlcWithRevocationSig(commitKeys, sig, redeemScript) - case _: RedeemInfo.TaprootKeyPath => ??? - case _: RedeemInfo.TaprootScriptPath => ??? + case _: RedeemInfo.TaprootKeyPath => Script.witnessKeyPathPay2tr(sig, sighash(TxOwner.Local, commitmentFormat)) + case s: RedeemInfo.TaprootScriptPath => Script.witnessScriptPathPay2tr(s.internalKey, s.leaf, ScriptWitness(Seq(sig)), s.scriptTree) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -880,15 +1043,18 @@ object Transactions { val redeemInfos: Map[ByteVector, HtlcPenaltyRedeemDetails] = htlcs.flatMap { case (paymentHash, htlcExpiry) => // We don't know if this was an incoming or outgoing HTLC, so we try both cases. - commitmentFormat match { + val (offered, received) = commitmentFormat match { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => - val offered = RedeemInfo.P2wsh(Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat))) - val received = RedeemInfo.P2wsh(Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat))) - Seq( - offered.pubkeyScript -> HtlcPenaltyRedeemDetails(offered, paymentHash, htlcExpiry, commitmentFormat.htlcOfferedPenaltyWeight), - received.pubkeyScript -> HtlcPenaltyRedeemDetails(received, paymentHash, htlcExpiry, commitmentFormat.htlcReceivedPenaltyWeight), - ) + (RedeemInfo.P2wsh(Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat))), + RedeemInfo.P2wsh(Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat)))) + case _: SimpleTaprootChannelCommitmentFormat => + (RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash).scriptTree)), + RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry).scriptTree))) } + Seq( + offered.pubkeyScript -> HtlcPenaltyRedeemDetails(offered, paymentHash, htlcExpiry, commitmentFormat.htlcOfferedPenaltyWeight), + received.pubkeyScript -> HtlcPenaltyRedeemDetails(received, paymentHash, htlcExpiry, commitmentFormat.htlcReceivedPenaltyWeight), + ) }.toMap // We check every output of the commitment transaction, and create an HTLC-penalty transaction if it is an HTLC output. commitTx.txOut.zipWithIndex.collect { @@ -930,6 +1096,10 @@ object Transactions { val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) val sig = sign(revocationKey, sighash(TxOwner.Local, commitmentFormat), RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val redeemInfo = RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toRemoteDelay))) + val sig = sign(revocationKey, sighash(TxOwner.Local, commitmentFormat), redeemInfo, extraUtxos = Map.empty) + Script.witnessKeyPathPay2tr(sig) } copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -948,6 +1118,8 @@ object Transactions { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toRemoteDelay))) } // Note that we check *all* outputs of the tx, because it could spend a batch of HTLC outputs from the commit tx. htlcTx.txOut.zipWithIndex.collect { @@ -990,7 +1162,7 @@ object Transactions { def offeredHtlcTrimThreshold(dustLimit: Satoshi, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Satoshi = { commitmentFormat match { - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => dustLimit + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => dustLimit case _ => dustLimit + weight2fee(feerate, commitmentFormat.htlcTimeoutWeight) } } @@ -1008,7 +1180,7 @@ object Transactions { def receivedHtlcTrimThreshold(dustLimit: Satoshi, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Satoshi = { commitmentFormat match { - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => dustLimit + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => dustLimit case _ => dustLimit + weight2fee(feerate, commitmentFormat.htlcSuccessWeight) } } @@ -1044,7 +1216,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 } @@ -1098,7 +1270,7 @@ object Transactions { private 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 } @@ -1115,17 +1287,17 @@ object Transactions { trimOfferedHtlcs(dustLimit, spec, commitmentFormat).foreach { htlc => val fee = weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcTimeoutWeight) val amountAfterFees = htlc.add.amountMsat.truncateToSatoshi - fee - val redeemScript = htlcOffered(commitmentKeys, htlc.add.paymentHash, commitmentFormat) - val htlcDelayedScript = toLocalDelayed(commitmentKeys, toSelfDelay) - outputs.append(OutHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), TxOut(amountAfterFees, pay2wsh(htlcDelayedScript)))) + val redeemInfo = HtlcTimeoutTx.redeemInfo(commitmentKeys, htlc.add.paymentHash, commitmentFormat) + val htlcDelayedRedeemInfo = HtlcDelayedTx.redeemInfo(commitmentKeys, toSelfDelay, commitmentFormat) + outputs.append(OutHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi, redeemInfo.pubkeyScript), TxOut(amountAfterFees, htlcDelayedRedeemInfo.pubkeyScript))) } trimReceivedHtlcs(dustLimit, spec, commitmentFormat).foreach { htlc => val fee = weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcSuccessWeight) val amountAfterFees = htlc.add.amountMsat.truncateToSatoshi - fee - val redeemScript = htlcReceived(commitmentKeys, htlc.add.paymentHash, htlc.add.cltvExpiry, commitmentFormat) - val htlcDelayedScript = toLocalDelayed(commitmentKeys, toSelfDelay) - outputs.append(InHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), TxOut(amountAfterFees, pay2wsh(htlcDelayedScript)))) + val redeemInfo = HtlcSuccessTx.redeemInfo(commitmentKeys, htlc.add.paymentHash, htlc.add.cltvExpiry, commitmentFormat) + val htlcDelayedRedeemInfo = HtlcDelayedTx.redeemInfo(commitmentKeys, toSelfDelay, commitmentFormat) + outputs.append(InHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi, redeemInfo.pubkeyScript), TxOut(amountAfterFees, htlcDelayedRedeemInfo.pubkeyScript))) } val hasHtlcs = outputs.nonEmpty @@ -1137,32 +1309,40 @@ object Transactions { } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway if (toLocalAmount >= dustLimit) { - val redeemScript = toLocalDelayed(commitmentKeys, toSelfDelay) - outputs.append(ToLocal(TxOut(toLocalAmount, pay2wsh(redeemScript)))) + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => + RedeemInfo.P2wsh(toLocalDelayed(commitmentKeys, toSelfDelay)) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitmentKeys, toSelfDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + } + outputs.append(ToLocal(TxOut(toLocalAmount, redeemInfo.pubkeyScript))) } if (toRemoteAmount >= dustLimit) { - commitmentFormat match { + val redeemInfo = commitmentFormat match { case DefaultCommitmentFormat => - val redeemKey = commitmentKeys.remotePaymentPublicKey - outputs.append(ToRemote(TxOut(toRemoteAmount, pay2wpkh(redeemKey)))) + RedeemInfo.P2wpkh(commitmentKeys.remotePaymentPublicKey) case _: AnchorOutputsCommitmentFormat => - val redeemScript = toRemoteDelayed(commitmentKeys) - outputs.append(ToRemote(TxOut(toRemoteAmount, pay2wsh(redeemScript)))) + RedeemInfo.P2wsh(toRemoteDelayed(commitmentKeys)) + case _: SimpleTaprootChannelCommitmentFormat => + val scripTree = Taproot.toRemoteScriptTree(commitmentKeys) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scripTree, scripTree.hash()) } + outputs.append(ToRemote(TxOut(toRemoteAmount, redeemInfo.pubkeyScript))) } commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => + case DefaultCommitmentFormat => () + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => if (toLocalAmount >= dustLimit || hasHtlcs) { - val redeemScript = anchor(localFundingPublicKey) - outputs.append(ToLocalAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(redeemScript)))) + val redeemInfo = ClaimAnchorOutputTx.redeemInfo(localFundingPublicKey, commitmentKeys, toLocal = true, commitmentFormat) + outputs.append(ToLocalAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } if (toRemoteAmount >= dustLimit || hasHtlcs) { - val redeemScript = anchor(remoteFundingPublicKey) - outputs.append(ToRemoteAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(redeemScript)))) + val redeemInfo = ClaimAnchorOutputTx.redeemInfo(remoteFundingPublicKey, commitmentKeys, toLocal = false, commitmentFormat) + outputs.append(ToRemoteAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } - case _ => } outputs.sortWith(CommitmentOutput.isLessThan).toSeq @@ -1288,5 +1468,4 @@ object Transactions { */ val PlaceHolderSig: ByteVector64 = ByteVector64(ByteVector.fill(64)(0xaa)) assert(der(PlaceHolderSig).size == 72) - } 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 e7f834c511..bd93900e7f 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 @@ -1604,7 +1604,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(remoteCommitTx.txOut.size == 4) - case _: AnchorOutputsCommitmentFormat => assert(remoteCommitTx.txOut.size == 6) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(remoteCommitTx.txOut.size == 6) } probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx)) @@ -1615,7 +1615,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val anchorTx_opt = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => None - case _: AnchorOutputsCommitmentFormat => Some(alice2blockchain.expectMsgType[PublishReplaceableTx]) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(alice2blockchain.expectMsgType[PublishReplaceableTx]) } if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) 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 8bd7d28e4e..3b5e46e705 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 @@ -594,7 +594,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]) @@ -633,7 +633,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // If anchor outputs is used, we use the anchor output to bump the fees if necessary. val anchorTx_opt = closingData.commitments.params.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => Some(s2blockchain.expectMsgType[PublishReplaceableTx]) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(s2blockchain.expectMsgType[PublishReplaceableTx]) case Transactions.DefaultCommitmentFormat => None } anchorTx_opt.foreach(anchor => assert(anchor.tx.isInstanceOf[ReplaceableRemoteCommitAnchor])) 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 0e23ffa827..77a4549802 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 fd778546c9..d62d60b40f 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 7c7bf41f5a..7751424204 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 1e4bb125d2..3fb489ae7e 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 @@ -71,7 +71,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 63c1c36dcf..3807bbf3d9 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 @@ -1265,7 +1265,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 | _: 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) @@ -1400,7 +1400,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) @@ -1597,7 +1597,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) @@ -1671,7 +1671,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 } @@ -1687,7 +1687,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) } @@ -1703,7 +1703,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) } @@ -1717,7 +1717,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) } @@ -2089,7 +2089,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 3a0132e1fc..5db35ffdbf 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} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} 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 | _: 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 460e3dd8b7..de01cc5cd7 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 @@ -18,11 +18,12 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.scalacompat.Crypto._ -import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, Musig2, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OP_RETURN, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi} -import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash, SigVersion} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, Musig2, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OP_RETURN, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.{ScriptFlags, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.transactions.CommitmentOutput.OutHtlc @@ -34,7 +35,6 @@ import grizzled.slf4j.Logging import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ -import java.nio.ByteOrder import scala.io.Source import scala.util.{Random, Try} @@ -144,10 +144,14 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(dummyTx.copy(txOut = dummyTx.txOut ++ Seq(p2wpkhOutput)).weight() - dummyTx.weight() == p2wpkhOutputWeight) } - private def checkExpectedWeight(actual: Int, expected: Int): Unit = { - // ECDSA signatures are der-encoded, which creates some variability in signature size compared to the baseline. - assert(actual <= expected + 2) - assert(actual >= expected - 2) + private def checkExpectedWeight(actual: Int, expected: Int, commitmentFormat: CommitmentFormat): Unit = { + commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => assert(actual == expected) + case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => + // ECDSA signatures are der-encoded, which creates some variability in signature size compared to the baseline. + assert(actual <= expected + 2) + assert(actual >= expected - 2) + } } test("generate valid commitment with some outputs that don't materialize (default commitment format)") { @@ -186,172 +190,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(outputs.isEmpty) } } - - test("generate valid commitment and htlc transactions (default commitment format)") { - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, DefaultCommitmentFormat) - - // htlc1 and htlc2 are regular IN/OUT htlcs - val paymentPreimage1 = randomBytes32() - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage2 = randomBytes32() - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(200).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) - // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage - val paymentPreimage3 = randomBytes32() - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, DefaultCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage4 = randomBytes32() - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, DefaultCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - // htlc5 and htlc6 are dust IN/OUT htlcs - val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket, None, 1.0, None) - val spec = CommitmentSpec( - htlcs = Set( - OutgoingHtlc(htlc1), - IncomingHtlc(htlc2), - OutgoingHtlc(htlc3), - IncomingHtlc(htlc4), - OutgoingHtlc(htlc5), - IncomingHtlc(htlc6) - ), - commitTxFeerate = feeratePerKw, - toLocal = 400.millibtc.toMilliSatoshi, - toRemote = 300.millibtc.toMilliSatoshi) - - val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, DefaultCommitmentFormat) - - val commitTxNumber = 0x404142434445L - val commitTx = { - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey) - val remoteSig = txInfo.sign(remoteFundingPriv, localFundingPriv.publicKey) - txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - } - - { - assert(getCommitTxNumber(commitTx, localIsChannelOpener = true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) - val hash = Crypto.sha256(localPaymentPriv.publicKey.value ++ remotePaymentPriv.publicKey.value) - val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL - val check = ((commitTx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.lockTime & 0xffffff) - assert((check ^ num) == commitTxNumber) - } - - val htlcTxs = makeHtlcTxs(commitTx, outputs, DefaultCommitmentFormat) - assert(htlcTxs.length == 4) - val expiries = htlcTxs.map(tx => tx.htlcId -> tx.htlcExpiry.toLong).toMap - assert(expiries == Map(0 -> 300, 1 -> 310, 2 -> 295, 3 -> 300)) - val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } - val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } - assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 2)) - assert(htlcSuccessTxs.size == 2) // htlc2 and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 3)) - - { - // either party spends local->remote htlc output with htlc timeout tx - for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localKeys, DefaultCommitmentFormat, Map.empty) - val remoteSig = htlcTimeoutTx.sign(remoteKeys, DefaultCommitmentFormat) - val signed = htlcTimeoutTx.addSigs(localKeys, localSig, remoteSig, DefaultCommitmentFormat) - assert(signed.validate(Map.empty)) - } - } - { - // local spends delayed output of htlc1 timeout tx - val Right(htlcDelayed) = HtlcDelayedTx.createSignedTx(localKeys, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(htlcDelayed.tx.weight(), DefaultCommitmentFormat.htlcDelayedWeight) - assert(htlcDelayed.validate(Map.empty)) - // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val htlcDelayed1 = HtlcDelayedTx.createSignedTx(localKeys, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - assert(htlcDelayed1 == Left(AmountBelowDustLimit)) - } - { - // remote spends local->remote htlc1/htlc3 output directly in case of success - for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val Right(claimHtlcSuccessTx) = ClaimHtlcSuccessTx.createSignedTx(remoteKeys, commitTx, localDustLimit, outputs, finalPubKeyScript, htlc, paymentPreimage, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(claimHtlcSuccessTx.tx.weight(), DefaultCommitmentFormat.claimHtlcSuccessWeight) - assert(claimHtlcSuccessTx.validate(Map.empty)) - } - } - { - // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage - for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(0), paymentPreimage4) :: Nil) { - val localSig = htlcSuccessTx.sign(localKeys, DefaultCommitmentFormat, Map.empty) - val remoteSig = htlcSuccessTx.sign(remoteKeys, DefaultCommitmentFormat) - val signedTx = htlcSuccessTx.addSigs(localKeys, localSig, remoteSig, paymentPreimage, DefaultCommitmentFormat) - assert(signedTx.validate(Map.empty)) - // check remote sig - assert(htlcSuccessTx.checkRemoteSig(localKeys, remoteSig, DefaultCommitmentFormat)) - } - } - { - // local spends delayed output of htlc2 success tx - val Right(htlcDelayed) = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(htlcDelayed.tx.weight(), DefaultCommitmentFormat.htlcDelayedWeight) - assert(htlcDelayed.validate(Map.empty)) - // local can't claim delayed output of htlc4 success tx because it is below the dust limit - val htlcDelayed1 = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - assert(htlcDelayed1 == Left(AmountBelowDustLimit)) - } - { - // local spends main delayed output - val Right(claimMainOutputTx) = ClaimLocalDelayedOutputTx.createSignedTx(localKeys, commitTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(claimMainOutputTx.tx.weight(), DefaultCommitmentFormat.toLocalDelayedWeight) - assert(claimMainOutputTx.validate(Map.empty)) - } - { - // remote spends main output - val Right(claimP2WPKHOutputTx) = ClaimP2WPKHOutputTx.createSignedTx(remoteKeys, commitTx, localDustLimit, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(claimP2WPKHOutputTx.tx.weight(), DefaultCommitmentFormat.toRemoteWeight) - assert(claimP2WPKHOutputTx.validate(Map.empty)) - } - { - // remote spends remote->local htlc output directly in case of timeout - val Right(claimHtlcTimeoutTx) = ClaimHtlcTimeoutTx.createSignedTx(remoteKeys, commitTx, localDustLimit, outputs, finalPubKeyScript, htlc2, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(claimHtlcTimeoutTx.tx.weight(), DefaultCommitmentFormat.claimHtlcTimeoutWeight) - assert(claimHtlcTimeoutTx.validate(Map.empty)) - } - { - // remote spends local main delayed output with revocation key - val Right(mainPenaltyTx) = MainPenaltyTx.createSignedTx(remoteKeys, localRevocationPriv, commitTx, localDustLimit, finalPubKeyScript, toLocalDelay, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(mainPenaltyTx.tx.weight(), DefaultCommitmentFormat.mainPenaltyWeight) - assert(mainPenaltyTx.validate(Map.empty)) - } - { - // remote spends HTLC outputs with revocation key - val htlcs = spec.htlcs.map(_.add).map(add => (add.paymentHash, add.cltvExpiry)).toSeq - val htlcPenaltyTxs = HtlcPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, commitTx, htlcs, localDustLimit, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - assert(htlcPenaltyTxs.collect { case Right(htlcPenaltyTx) => htlcPenaltyTx.paymentHash }.toSet == Set(htlc1, htlc2, htlc3, htlc4).map(_.paymentHash)) // the first 4 htlcs are above the dust limit - htlcPenaltyTxs.collect { - case Right(htlcPenaltyTx) => - val expectedWeight = if (Set(htlc1, htlc3).map(_.paymentHash).contains(htlcPenaltyTx.paymentHash)) { - DefaultCommitmentFormat.htlcOfferedPenaltyWeight - } else { - DefaultCommitmentFormat.htlcReceivedPenaltyWeight - } - checkExpectedWeight(htlcPenaltyTx.tx.weight(), expectedWeight) - assert(htlcPenaltyTx.validate(Map.empty)) - } - } - { - // remote spends htlc1's htlc-timeout tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTx)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(claimHtlcDelayedPenaltyTx.tx.weight(), DefaultCommitmentFormat.claimHtlcPenaltyWeight) - assert(claimHtlcDelayedPenaltyTx.validate(Map.empty)) - // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) - } - { - // remote spends htlc2's htlc-success tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTx)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - checkExpectedWeight(claimHtlcDelayedPenaltyTx.tx.weight(), DefaultCommitmentFormat.claimHtlcPenaltyWeight) - assert(claimHtlcDelayedPenaltyTx.validate(Map.empty)) - // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat) - assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) - } - } - + test("generate valid commitment with some outputs that don't materialize (anchor outputs)") { val spec = CommitmentSpec(htlcs = Set.empty, commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) val commitFeeAndAnchorCost = commitTxTotalCost(localDustLimit, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) @@ -401,29 +240,31 @@ class TransactionsSpec extends AnyFunSuite with Logging { } } - test("generate valid commitment and htlc transactions (anchor outputs)") { + def `generate valid commitment and htlc transactions`(commitmentFormat: CommitmentFormat): Unit = { val walletPriv = randomKey() val walletPub = walletPriv.publicKey val finalPubKeyScript = Script.write(Script.pay2wpkh(walletPub)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, UnsafeLegacyAnchorOutputsCommitmentFormat) + val fundingInfo = Funding.makeFundingScript(localFundingPriv.publicKey, remoteFundingPriv.publicKey, commitmentFormat) + val fundingTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fundingInfo.pubkeyScript) :: Nil, lockTime = 0) + val fundingTxOutpoint = OutPoint(fundingTx.txid, 0) + val commitInput = Funding.makeFundingInputInfo(fundingTxOutpoint.txid, fundingTxOutpoint.index.toInt, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, commitmentFormat) + + val paymentPreimages = Seq(randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32()) + val paymentPreimageMap = paymentPreimages.map(p => sha256(p) -> p).toMap // htlc1, htlc2a and htlc2b are regular IN/OUT htlcs - val paymentPreimage1 = randomBytes32() - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage2 = randomBytes32() - val htlc2a = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(50).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliBtc(150).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimages(0)), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc2a = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(50).toMilliSatoshi, sha256(paymentPreimages(1)), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliBtc(150).toMilliSatoshi, sha256(paymentPreimages(1)), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage - val paymentPreimage3 = randomBytes32() - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage4 = randomBytes32() - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, commitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimages(2)), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, commitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimages(3)), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) // htlc5 and htlc6 are dust IN/OUT htlcs - val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(paymentPreimages(4)), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(paymentPreimages(5)), CltvExpiry(305), TestConstants.emptyOnionPacket, None, 1.0, None) // htlc7 and htlc8 are at the dust limit when we ignore 2nd-stage tx fees - val htlc7 = UpdateAddHtlc(ByteVector32.Zeroes, 7, localDustLimit.toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc8 = UpdateAddHtlc(ByteVector32.Zeroes, 8, localDustLimit.toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(302), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc7 = UpdateAddHtlc(ByteVector32.Zeroes, 7, localDustLimit.toMilliSatoshi, sha256(paymentPreimages(6)), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc8 = UpdateAddHtlc(ByteVector32.Zeroes, 8, localDustLimit.toMilliSatoshi, sha256(paymentPreimages(7)), CltvExpiry(302), TestConstants.emptyOnionPacket, None, 1.0, None) val spec = CommitmentSpec( htlcs = Set( OutgoingHtlc(htlc1), @@ -439,69 +280,83 @@ class TransactionsSpec extends AnyFunSuite with Logging { commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) + val (secretLocalNonce, publicLocalNonce) = Musig2.generateNonce(randomBytes32(), localFundingPriv, Seq(localFundingPriv.publicKey)) + val (secretRemoteNonce, publicRemoteNonce) = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, Seq(remoteFundingPriv.publicKey)) + val publicNonces = Seq(publicLocalNonce, publicRemoteNonce) val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, commitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey) - val remoteSig = txInfo.sign(remotePaymentPriv, localFundingPriv.publicKey) - val commitTx = txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - - val htlcTxs = makeHtlcTxs(commitTx, outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(htlcTxs.length == 5) + val commitTx = commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + val Right(commitTx) = for { + localPartialSig <- txInfo.partialSign(localFundingPriv, remoteFundingPriv.publicKey, Map.empty, LocalNonce(secretLocalNonce, publicLocalNonce), publicNonces) + remotePartialSig <- txInfo.partialSign(remoteFundingPriv, localFundingPriv.publicKey, Map.empty, LocalNonce(secretRemoteNonce, publicRemoteNonce), publicNonces) + _ = assert(txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, remotePartialSig, publicLocalNonce)) + invalidRemotePartialSig = ChannelSpendSignature.PartialSignatureWithNonce(randomBytes32(), remotePartialSig.nonce) + _ = assert(!txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, invalidRemotePartialSig, publicLocalNonce)) + tx <- txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localPartialSig, remotePartialSig, Map.empty) + } yield tx + commitTx + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey) + val remoteSig = txInfo.sign(remoteFundingPriv, localFundingPriv.publicKey) + assert(txInfo.checkRemoteSig(localFundingPubkey = localFundingPriv.publicKey, remoteFundingPriv.publicKey, remoteSig)) + val invalidRemoteSig = ChannelSpendSignature.IndividualSignature(randomBytes64()) + assert(!txInfo.checkRemoteSig(localFundingPubkey = localFundingPriv.publicKey, remoteFundingPriv.publicKey, invalidRemoteSig)) + val commitTx = txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) + commitTx + } + commitTx.correctlySpends(Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcTxs = makeHtlcTxs(commitTx, outputs, commitmentFormat) val expiries = htlcTxs.map(tx => tx.htlcId -> tx.htlcExpiry.toLong).toMap - assert(expiries == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } - assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3)) - assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4)) - - val zeroFeeOutputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, zeroFeeOutputs) - val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, zeroFeeOutputs, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(zeroFeeHtlcTxs.length == 7) - val zeroFeeExpiries = zeroFeeHtlcTxs.map(tx => tx.htlcId -> tx.htlcExpiry.toLong).toMap - assert(zeroFeeExpiries == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) - val zeroFeeHtlcSuccessTxs = zeroFeeHtlcTxs.collect { case tx: HtlcSuccessTx => tx } - val zeroFeeHtlcTimeoutTxs = zeroFeeHtlcTxs.collect { case tx: HtlcTimeoutTx => tx } - zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee == 0.sat)) - zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee == 0.sat)) - assert(zeroFeeHtlcTimeoutTxs.size == 3) // htlc1, htlc3 and htlc7 - assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3, 7)) - assert(zeroFeeHtlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 - assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4, 8)) - + commitmentFormat match { + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => + assert(htlcTxs.length == 7) + assert(expiries == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) + assert(htlcTimeoutTxs.size == 3) // htlc1 and htlc3 and htlc7 + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3, 7)) + assert(htlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4, 8)) + case _ => + assert(htlcTxs.length == 5) + assert(expiries == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) + assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3)) + assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4)) + } (commitTx, outputs, htlcTimeoutTxs, htlcSuccessTxs) } { // local spends main delayed output - val Right(claimMainOutputTx) = ClaimLocalDelayedOutputTx.createSignedTx(localKeys, commitTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(claimMainOutputTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.toLocalDelayedWeight) + val Right(claimMainOutputTx) = ClaimLocalDelayedOutputTx.createSignedTx(localKeys, commitTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + checkExpectedWeight(claimMainOutputTx.tx.weight(), commitmentFormat.toLocalDelayedWeight, commitmentFormat) assert(claimMainOutputTx.validate(Map.empty)) } - { + if (commitmentFormat != DefaultCommitmentFormat) { // remote cannot spend main output with default commitment format - val Left(failure) = ClaimP2WPKHOutputTx.createSignedTx(remoteKeys, commitTx, localDustLimit, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val Left(failure) = ClaimP2WPKHOutputTx.createSignedTx(remoteKeys, commitTx, localDustLimit, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(failure == OutputNotFound) } - { + if (commitmentFormat != DefaultCommitmentFormat) { // remote spends main delayed output - val Right(claimRemoteDelayedOutputTx) = ClaimRemoteDelayedOutputTx.createSignedTx(remoteKeys, commitTx, localDustLimit, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(claimRemoteDelayedOutputTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.toRemoteWeight) + val Right(claimRemoteDelayedOutputTx) = ClaimRemoteDelayedOutputTx.createSignedTx(remoteKeys, commitTx, localDustLimit, finalPubKeyScript, feeratePerKw, commitmentFormat) + checkExpectedWeight(claimRemoteDelayedOutputTx.tx.weight(), commitmentFormat.toRemoteWeight, commitmentFormat) assert(claimRemoteDelayedOutputTx.validate(Map.empty)) } - { + if (commitmentFormat != DefaultCommitmentFormat) { // local spends local anchor with additional wallet inputs val walletAmount = 50_000 sat val walletInputs = Map( OutPoint(randomTxId(), 3) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), OutPoint(randomTxId(), 0) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), ) - val Right(claimAnchorOutputTx) = ClaimAnchorOutputTx.createUnsignedTx(localFundingPriv, localKeys.publicKeys, commitTx, UnsafeLegacyAnchorOutputsCommitmentFormat).map(anchorTx => { + val Right(claimAnchorOutputTx) = ClaimAnchorOutputTx.createUnsignedTx(localFundingPriv, localKeys, commitTx, commitmentFormat).map(anchorTx => { val walletTxIn = walletInputs.map { case (outpoint, _) => TxIn(outpoint, ByteVector.empty, 0) } val unsignedTx = anchorTx.tx.copy(txIn = anchorTx.tx.txIn ++ walletTxIn) val sig1 = unsignedTx.signInput(1, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) @@ -514,113 +369,121 @@ class TransactionsSpec extends AnyFunSuite with Logging { val allInputs = walletInputs + (claimAnchorOutputTx.input.outPoint -> claimAnchorOutputTx.input.txOut) assert(Try(Transaction.correctlySpends(claimAnchorOutputTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isFailure) // All wallet inputs must be provided when signing. - assert(Try(claimAnchorOutputTx.sign(localFundingPriv, localKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty)).isFailure) - assert(Try(claimAnchorOutputTx.sign(localFundingPriv, localKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs.take(1))).isFailure) - val signedTx = claimAnchorOutputTx.sign(localFundingPriv, localKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs) + assert(Try(claimAnchorOutputTx.sign(localFundingPriv, localKeys, commitmentFormat, Map.empty)).isFailure) + assert(Try(claimAnchorOutputTx.sign(localFundingPriv, localKeys, commitmentFormat, walletInputs.take(1))).isFailure) + val signedTx = claimAnchorOutputTx.sign(localFundingPriv, localKeys, commitmentFormat, walletInputs) val anchorInputWeight = signedTx.tx.weight() - signedTx.tx.copy(txIn = signedTx.tx.txIn.tail).weight() - checkExpectedWeight(anchorInputWeight, UnsafeLegacyAnchorOutputsCommitmentFormat.anchorInputWeight) + checkExpectedWeight(anchorInputWeight, commitmentFormat.anchorInputWeight, commitmentFormat) Transaction.correctlySpends(signedTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - { + if (commitmentFormat != DefaultCommitmentFormat) { // remote spends remote anchor - val Right(claimAnchorOutputTx) = ClaimAnchorOutputTx.createUnsignedTx(remoteFundingPriv, remoteKeys.publicKeys, commitTx, UnsafeLegacyAnchorOutputsCommitmentFormat) + val Right(claimAnchorOutputTx) = ClaimAnchorOutputTx.createUnsignedTx(remoteFundingPriv, remoteKeys, commitTx, commitmentFormat) assert(!claimAnchorOutputTx.validate(Map.empty)) - val signedTx = claimAnchorOutputTx.sign(remoteFundingPriv, remoteKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val signedTx = claimAnchorOutputTx.sign(remoteFundingPriv, remoteKeys, commitmentFormat, Map.empty) assert(signedTx.validate(Map.empty)) } { // remote spends local main delayed output with revocation key - val Right(mainPenaltyTx) = MainPenaltyTx.createSignedTx(remoteKeys, localRevocationPriv, commitTx, localDustLimit, finalPubKeyScript, toLocalDelay, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(mainPenaltyTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.mainPenaltyWeight) + val Right(mainPenaltyTx) = MainPenaltyTx.createSignedTx(remoteKeys, localRevocationPriv, commitTx, localDustLimit, finalPubKeyScript, toLocalDelay, feeratePerKw, commitmentFormat) + checkExpectedWeight(mainPenaltyTx.tx.weight(), commitmentFormat.mainPenaltyWeight, commitmentFormat) assert(mainPenaltyTx.validate(Map.empty)) } { // local spends received htlc with HTLC-timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = htlcTimeoutTx.sign(remoteKeys, UnsafeLegacyAnchorOutputsCommitmentFormat) - val signedTx = htlcTimeoutTx.addSigs(localKeys, localSig, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = htlcTimeoutTx.sign(localKeys, commitmentFormat, Map.empty) + val remoteSig = htlcTimeoutTx.sign(remoteKeys, commitmentFormat) + val signedTx = htlcTimeoutTx.addSigs(localKeys, localSig, remoteSig, commitmentFormat) assert(signedTx.validate(Map.empty)) // local detects when remote doesn't use the right sighash flags - val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + val invalidSighash = commitmentFormat match { + case DefaultCommitmentFormat => Seq(SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + case _ => Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + } for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcTimeoutTx.signWithInvalidSighash(remoteKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, sighash) - val invalidTx = htlcTimeoutTx.addSigs(localKeys, localSig, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) + val invalidRemoteSig = htlcTimeoutTx.signWithInvalidSighash(remoteKeys, commitmentFormat, sighash) + val invalidTx = htlcTimeoutTx.addSigs(localKeys, localSig, invalidRemoteSig, commitmentFormat) assert(!invalidTx.validate(Map.empty)) } } } { // local spends delayed output of htlc1 timeout tx - val Right(htlcDelayed) = HtlcDelayedTx.createSignedTx(localKeys, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(htlcDelayed.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.htlcDelayedWeight) + val Right(htlcDelayed) = HtlcDelayedTx.createSignedTx(localKeys, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + checkExpectedWeight(htlcDelayed.tx.weight(), commitmentFormat.htlcDelayedWeight, commitmentFormat) assert(htlcDelayed.validate(Map.empty)) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val htlcDelayed1 = HtlcDelayedTx.createSignedTx(localKeys, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val htlcDelayed1 = HtlcDelayedTx.createSignedTx(localKeys, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(htlcDelayed1 == Left(AmountBelowDustLimit)) } { // local spends offered htlc with HTLC-success tx - for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = htlcSuccessTx.sign(localKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = htlcSuccessTx.sign(remoteKeys, UnsafeLegacyAnchorOutputsCommitmentFormat) - val signedTx = htlcSuccessTx.addSigs(localKeys, localSig, remoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) + for (htlcSuccessTx <- htlcSuccessTxs(0) :: htlcSuccessTxs(1) :: htlcSuccessTxs(2) :: Nil) { + val paymentPreimage = paymentPreimageMap(htlcSuccessTx.paymentHash) + val localSig = htlcSuccessTx.sign(localKeys, commitmentFormat, Map.empty) + val remoteSig = htlcSuccessTx.sign(remoteKeys, commitmentFormat) + val signedTx = htlcSuccessTx.addSigs(localKeys, localSig, remoteSig, paymentPreimage, commitmentFormat) assert(signedTx.validate(Map.empty)) // check remote sig - assert(htlcSuccessTx.checkRemoteSig(localKeys, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(htlcSuccessTx.checkRemoteSig(localKeys, remoteSig, commitmentFormat)) // local detects when remote doesn't use the right sighash flags - val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + val invalidSighash = commitmentFormat match { + case DefaultCommitmentFormat => Seq(SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + case _ => Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + } for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcSuccessTx.signWithInvalidSighash(remoteKeys, UnsafeLegacyAnchorOutputsCommitmentFormat, sighash) - val invalidTx = htlcSuccessTx.addSigs(localKeys, localSig, invalidRemoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) + val invalidRemoteSig = htlcSuccessTx.signWithInvalidSighash(remoteKeys, commitmentFormat, sighash) + val invalidTx = htlcSuccessTx.addSigs(localKeys, localSig, invalidRemoteSig, paymentPreimage, commitmentFormat) assert(!invalidTx.validate(Map.empty)) - assert(!invalidTx.checkRemoteSig(localKeys, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(!invalidTx.checkRemoteSig(localKeys, invalidRemoteSig, commitmentFormat)) } } } { // local spends delayed output of htlc2a and htlc2b success txs - val Right(htlcDelayedA) = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val Right(htlcDelayedB) = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(2).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - Seq(htlcDelayedA, htlcDelayedB).foreach(htlcDelayed => checkExpectedWeight(htlcDelayed.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.htlcDelayedWeight)) + val Right(htlcDelayedA) = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + val Right(htlcDelayedB) = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(2).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + Seq(htlcDelayedA, htlcDelayedB).foreach(htlcDelayed => checkExpectedWeight(htlcDelayed.tx.weight(), commitmentFormat.htlcDelayedWeight, commitmentFormat)) Seq(htlcDelayedA, htlcDelayedB).foreach(htlcDelayed => assert(htlcDelayed.validate(Map.empty))) // local can't claim delayed output of htlc4 success tx because it is below the dust limit - val htlcDelayedC = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val htlcDelayedC = HtlcDelayedTx.createSignedTx(localKeys, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(htlcDelayedC == Left(AmountBelowDustLimit)) } { // remote spends local->remote htlc outputs directly in case of success - for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val Right(claimHtlcSuccessTx) = ClaimHtlcSuccessTx.createSignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalPubKeyScript, htlc, paymentPreimage, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(claimHtlcSuccessTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.claimHtlcSuccessWeight) + for (htlc <- htlc1 :: htlc3 :: Nil) { + val paymentPreimage = paymentPreimageMap(htlc.paymentHash) + val Right(claimHtlcSuccessTx) = ClaimHtlcSuccessTx.createSignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalPubKeyScript, htlc, paymentPreimage, feeratePerKw, commitmentFormat) + checkExpectedWeight(claimHtlcSuccessTx.tx.weight(), commitmentFormat.claimHtlcSuccessWeight, commitmentFormat) assert(claimHtlcSuccessTx.validate(Map.empty)) } } { // remote spends htlc1's htlc-timeout tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTx)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(claimHtlcDelayedPenaltyTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.claimHtlcPenaltyWeight) + val Seq(Right(claimHtlcDelayedPenaltyTx)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + checkExpectedWeight(claimHtlcDelayedPenaltyTx.tx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) assert(claimHtlcDelayedPenaltyTx.validate(Map.empty)) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { - val Right(claimHtlcTimeoutTx) = ClaimHtlcTimeoutTx.createSignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - checkExpectedWeight(claimHtlcTimeoutTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.claimHtlcTimeoutWeight) + val Right(claimHtlcTimeoutTx) = ClaimHtlcTimeoutTx.createSignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalPubKeyScript, htlc, feeratePerKw, commitmentFormat) + checkExpectedWeight(claimHtlcTimeoutTx.tx.weight(), commitmentFormat.claimHtlcTimeoutWeight, commitmentFormat) assert(claimHtlcTimeoutTx.validate(Map.empty)) } } { // remote spends htlc2a/htlc2b's htlc-success tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTxA)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val Seq(Right(claimHtlcDelayedPenaltyTxB)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(2).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB).foreach(claimHtlcSuccessPenaltyTx => checkExpectedWeight(claimHtlcSuccessPenaltyTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.claimHtlcPenaltyWeight)) + val Seq(Right(claimHtlcDelayedPenaltyTxA)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + val Seq(Right(claimHtlcDelayedPenaltyTxB)) = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(2).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB).foreach(claimHtlcSuccessPenaltyTx => checkExpectedWeight(claimHtlcSuccessPenaltyTx.tx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat)) Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB).foreach(claimHtlcSuccessPenaltyTx => assert(claimHtlcSuccessPenaltyTx.validate(Map.empty))) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { @@ -628,276 +491,63 @@ class TransactionsSpec extends AnyFunSuite with Logging { val txIn = htlcTimeoutTxs.flatMap(_.tx.txIn) ++ htlcSuccessTxs.flatMap(_.tx.txIn) val txOut = htlcTimeoutTxs.flatMap(_.tx.txOut) ++ htlcSuccessTxs.flatMap(_.tx.txOut) val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) - val claimHtlcDelayedPenaltyTxs = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, aggregatedHtlcTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(claimHtlcDelayedPenaltyTxs.size == 5) + val claimHtlcDelayedPenaltyTxs = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, aggregatedHtlcTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) val skipped = claimHtlcDelayedPenaltyTxs.collect { case Left(reason) => reason } - assert(skipped.size == 2) - assert(skipped.toSet == Set(AmountBelowDustLimit)) val claimed = claimHtlcDelayedPenaltyTxs.collect { case Right(tx) => tx } - assert(claimed.size == 3) - assert(claimed.map(_.input.outPoint).toSet.size == 3) + commitmentFormat match { + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => + assert(claimHtlcDelayedPenaltyTxs.size == 7) + assert(skipped.size == 2) + assert(skipped.toSet == Set(AmountBelowDustLimit)) + assert(claimed.size == 5) + assert(claimed.map(_.input.outPoint).toSet.size == 5) + case _ => + assert(claimHtlcDelayedPenaltyTxs.size == 5) + assert(skipped.size == 2) + assert(skipped.toSet == Set(AmountBelowDustLimit)) + assert(claimed.size == 3) + assert(claimed.map(_.input.outPoint).toSet.size == 3) + } claimed.foreach { htlcPenaltyTx => - checkExpectedWeight(htlcPenaltyTx.tx.weight(), UnsafeLegacyAnchorOutputsCommitmentFormat.claimHtlcPenaltyWeight) + checkExpectedWeight(htlcPenaltyTx.tx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) assert(htlcPenaltyTx.validate(Map.empty)) } } { // remote spends htlc outputs with revocation key val htlcs = spec.htlcs.map(_.add).map(add => (add.paymentHash, add.cltvExpiry)).toSeq - val htlcPenaltyTxs = HtlcPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, commitTx, htlcs, localDustLimit, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val htlcPenaltyTxs = HtlcPenaltyTx.createSignedTxs(remoteKeys, localRevocationPriv, commitTx, htlcs, localDustLimit, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(htlcPenaltyTxs.collect { case Right(htlcPenaltyTx) => htlcPenaltyTx.paymentHash }.toSet == Set(htlc1, htlc2a, htlc2b, htlc3, htlc4).map(_.paymentHash)) // the first 5 htlcs are above the dust limit htlcPenaltyTxs.collect { case Right(htlcPenaltyTx) => htlcPenaltyTx }.foreach { htlcPenaltyTx => val expectedWeight = if (htlcTimeoutTxs.map(_.input.outPoint).toSet.contains(htlcPenaltyTx.input.outPoint)) { - UnsafeLegacyAnchorOutputsCommitmentFormat.htlcOfferedPenaltyWeight + commitmentFormat.htlcOfferedPenaltyWeight } else { - UnsafeLegacyAnchorOutputsCommitmentFormat.htlcReceivedPenaltyWeight + commitmentFormat.htlcReceivedPenaltyWeight } - checkExpectedWeight(htlcPenaltyTx.tx.weight(), expectedWeight) + checkExpectedWeight(htlcPenaltyTx.tx.weight(), expectedWeight, commitmentFormat) assert(htlcPenaltyTx.validate(Map.empty)) } } } - test("generate valid commitment and htlc transactions (taproot)") { - import fr.acinq.bitcoin.scalacompat.KotlinUtils._ - import fr.acinq.eclair.transactions.Scripts.Taproot - - // funding tx sends to musig2 aggregate of local and remote funding keys - val fundingTxOutpoint = OutPoint(randomTxId(), 0) - val fundingOutput = TxOut(Btc(1), Script.pay2tr(Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), None)) - - // offered HTLC - val preimage = ByteVector32.fromValidHex("01" * 32) - val paymentHash = Crypto.sha256(preimage) - - val txNumber = 0x404142434445L - val (sequence, lockTime) = encodeTxNumber(txNumber) - val commitTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(fundingTxOutpoint, Nil, sequence) :: Nil, - txOut = Seq( - TxOut(300.millibtc, Taproot.toLocal(localKeys.publicKeys, toLocalDelay)), - TxOut(400.millibtc, Taproot.toRemote(localKeys.publicKeys)), - TxOut(330.sat, Taproot.anchor(localKeys.publicKeys.localDelayedPaymentPublicKey)), - TxOut(330.sat, Taproot.anchor(localKeys.publicKeys.remotePaymentPublicKey)), - TxOut(25_000.sat, Taproot.offeredHtlc(localKeys.publicKeys, paymentHash)), - TxOut(15_000.sat, Taproot.receivedHtlc(localKeys.publicKeys, paymentHash, CltvExpiry(300))) - ), - lockTime - ) - - val (secretLocalNonce, publicLocalNonce) = Musig2.generateNonce(randomBytes32(), localFundingPriv, Seq(localFundingPriv.publicKey)) - val (secretRemoteNonce, publicRemoteNonce) = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, Seq(remoteFundingPriv.publicKey)) - val publicKeys = Scripts.sort(Seq(localFundingPriv.publicKey, remoteFundingPriv.publicKey)) - val publicNonces = Seq(publicLocalNonce, publicRemoteNonce) - val Right(sig) = for { - localPartialSig <- Musig2.signTaprootInput(localFundingPriv, tx, 0, Seq(fundingOutput), publicKeys, secretLocalNonce, publicNonces, None) - remotePartialSig <- Musig2.signTaprootInput(remoteFundingPriv, tx, 0, Seq(fundingOutput), publicKeys, secretRemoteNonce, publicNonces, None) - sig <- Musig2.aggregateTaprootSignatures(Seq(localPartialSig, remotePartialSig), tx, 0, Seq(fundingOutput), publicKeys, publicNonces, None) - } yield sig - - tx.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) - } - Transaction.correctlySpends(commitTx, Map(fundingTxOutpoint -> fundingOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - - val spendToLocalOutputTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 0), Seq(), sequence = toLocalDelay.toInt) :: Nil, - txOut = TxOut(300.millibtc, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.toLocalScriptTree(localKeys.publicKeys, toLocalDelay) - val sig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(commitTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.getLeft.hash()) - val witness = Script.witnessScriptPathPay2tr(Taproot.NUMS_POINT.xOnly, scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(sig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendToLocalOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val mainPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(300.millibtc, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.toLocalScriptTree(remoteKeys.publicKeys, toLocalDelay) - val sig = Transaction.signInputTaprootScriptPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.getRight.hash()) - val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(Taproot.NUMS_POINT), scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(sig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(mainPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendToRemoteOutputTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 1), Nil, sequence = 1) :: Nil, - txOut = TxOut(400.millibtc, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.toRemoteScriptTree(remoteKeys.publicKeys) - val sig = Transaction.signInputTaprootScriptPath(remotePaymentPriv, tx, 0, Seq(commitTx.txOut(1)), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(Taproot.NUMS_POINT.xOnly, scriptTree, ScriptWitness(Seq(sig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendToRemoteOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendLocalAnchorTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val sig = Transaction.signInputTaprootKeyPath(localDelayedPaymentPriv, tx, 0, Seq(commitTx.txOut(2)), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendLocalAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendLocalAnchorAfterDelayTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = 16) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - // after 16 blocks, anchor outputs can be spent without a signature BUT spenders still need to know the local/remote payment public key - val witness = Script.witnessScriptPathPay2tr(localDelayedPaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree, ScriptWitness.empty, Scripts.Taproot.anchorScriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendLocalAnchorAfterDelayTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendRemoteAnchorTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 3), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val sig = Transaction.signInputTaprootKeyPath(remotePaymentPriv, tx, 0, Seq(commitTx.txOut(3)), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendRemoteAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendRemoteAnchorAfterDelayTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 3), Nil, sequence = 16) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val witness = Script.witnessScriptPathPay2tr(remotePaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree, ScriptWitness.empty, Scripts.Taproot.anchorScriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendRemoteAnchorAfterDelayTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // Spend offered HTLC with HTLC-Timeout tx. - val htlcTimeoutTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 4), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(25_000.sat, Taproot.htlcDelayed(localKeys.publicKeys, toLocalDelay)) :: Nil, - lockTime = 300) - val scriptTree = Taproot.offeredHtlcScriptTree(localKeys.publicKeys, 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 witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcTimeoutTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val offeredHtlcPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 4), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.offeredHtlcScriptTree(remoteKeys.publicKeys, paymentHash) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(offeredHtlcPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendHtlcTimeoutTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcTimeoutTx, 0), Nil, sequence = toLocalDelay.toInt) :: Nil, - txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(localKeys.publicKeys, toLocalDelay) - val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(htlcTimeoutTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree, ScriptWitness(Seq(localSig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendHtlcTimeoutTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val htlcTimeoutPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcTimeoutTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(remoteKeys.publicKeys, toLocalDelay) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(htlcTimeoutTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcTimeoutPenaltyTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (legacy anchor outputs)") { + `generate valid commitment and htlc transactions`(UnsafeLegacyAnchorOutputsCommitmentFormat) + } - // Spend received HTLC with HTLC-Success tx. - val htlcSuccessTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 5), Nil, sequence = 1) :: Nil, - txOut = TxOut(15_000.sat, Taproot.htlcDelayed(localKeys.publicKeys, toLocalDelay)) :: Nil, - lockTime = 0) - val scriptTree = Taproot.receivedHtlcScriptTree(localKeys.publicKeys, paymentHash, CltvExpiry(300)) - val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY - val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(5)), sigHash, scriptTree.getRight.hash()), sigHash) - val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(5)), sigHash, scriptTree.getRight.hash()), sigHash) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig, preimage)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcSuccessTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (zero fee anchor outputs)") { + `generate valid commitment and htlc transactions`(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } - val receivedHtlcPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 5), Nil, sequence = 1) :: Nil, - txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.receivedHtlcScriptTree(remoteKeys.publicKeys, paymentHash, CltvExpiry(300)) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(5)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(receivedHtlcPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (default commitment format)") { + `generate valid commitment and htlc transactions`(DefaultCommitmentFormat) + } - val spendHtlcSuccessTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcSuccessTx, 0), Nil, sequence = toLocalDelay.toInt) :: Nil, - txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(localKeys.publicKeys, toLocalDelay) - val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(htlcSuccessTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree, ScriptWitness(Seq(localSig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendHtlcSuccessTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (simple taproot channels)") { + `generate valid commitment and htlc transactions`(LegacySimpleTaprootChannelCommitmentFormat) + } - val htlcSuccessPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcSuccessTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(remoteKeys.publicKeys, toLocalDelay) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(htlcSuccessTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcSuccessPenaltyTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (zero fee simple taproot channels)") { + `generate valid commitment and htlc transactions`(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("generate taproot NUMS point") { @@ -965,8 +615,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, DefaultCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey) - val remoteSig = txInfo.sign(remotePaymentPriv, localFundingPriv.publicKey) + val remoteSig = txInfo.sign(remoteFundingPriv, localFundingPriv.publicKey) val commitTx = txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) + commitTx.correctlySpends(Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val htlcTxs = makeHtlcTxs(commitTx, outputs, DefaultCommitmentFormat) (commitTx, outputs, htlcTxs) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index db9e4034b7..eef61fb583 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -247,7 +247,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // make sure that we have extracted the remote sig of the local tx val remoteSig = newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.remoteSig val commitTx = newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - assert(commitTx.checkRemoteSig(testCase.localFundingPublicKey, testCase.remoteFundingPublicKey, remoteSig)) + assert(commitTx.checkRemoteSig(testCase.localFundingPublicKey, testCase.remoteFundingPublicKey, remoteSig.asInstanceOf[ChannelSpendSignature.IndividualSignature])) } } diff --git a/pom.xml b/pom.xml index d21da71680..570f3de58c 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 2.6.20 10.2.7 3.8.16 - 0.37 + 0.39 32.1.1-jre 2.7.4 1.0.18