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