From 63bb2733b9e3407929f879322da0bb50328bdfda Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 19 May 2026 09:31:21 +0200 Subject: [PATCH 1/5] Upgrade to bitcoin-kmp 0.31.0 --- pom.xml | 16 +--------------- .../scalacompat/DeterministicWallet.scala | 14 +++++++------- .../fr/acinq/bitcoin/scalacompat/Musig2.scala | 4 +++- .../acinq/bitcoin/scalacompat/Musig2Spec.scala | 6 ++++++ 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index 7b014367..1aad819f 100644 --- a/pom.xml +++ b/pom.xml @@ -146,7 +146,7 @@ fr.acinq.bitcoin bitcoin-kmp-jvm - 0.30.0 + 0.31.0 fr.acinq.secp256k1 @@ -224,20 +224,6 @@ - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala index 98a23206..855d1858 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala @@ -119,10 +119,10 @@ object DeterministicWallet { } } - def encode(input: ExtendedPublicKey, prefix: Int): String = bitcoin.DeterministicWallet.encode(input.pub, prefix) + def encode(input: ExtendedPublicKey, prefix: Int): String = input.pub.encode(prefix) def write(input: ExtendedPublicKey, output: OutputStream): Unit = { - fr.acinq.bitcoin.DeterministicWallet.write(input.pub, OutputStreamWrapper(output)) + input.pub.write(OutputStreamWrapper(output)) } /** @@ -137,21 +137,21 @@ object DeterministicWallet { * @param input extended private key * @return the public key for this private key */ - def publicKey(input: ExtendedPrivateKey): ExtendedPublicKey = ExtendedPublicKey(bitcoin.DeterministicWallet.publicKey(input.priv)) + def publicKey(input: ExtendedPrivateKey): ExtendedPublicKey = ExtendedPublicKey(input.priv.extendedPublicKey) /** * * @param input extended public key * @return the fingerprint for this public key */ - def fingerprint(input: ExtendedPublicKey): Long = bitcoin.DeterministicWallet.fingerprint(input.pub) + def fingerprint(input: ExtendedPublicKey): Long = input.pub.fingerprint() /** * * @param input extended private key * @return the fingerprint for this private key (which is based on the corresponding public key) */ - def fingerprint(input: ExtendedPrivateKey): Long = bitcoin.DeterministicWallet.fingerprint(input.priv) + def fingerprint(input: ExtendedPrivateKey): Long = input.priv.fingerprint() /** * @@ -159,7 +159,7 @@ object DeterministicWallet { * @param index index of the child key * @return the derived private key at the specified index */ - def derivePrivateKey(parent: ExtendedPrivateKey, index: Long): ExtendedPrivateKey = ExtendedPrivateKey(bitcoin.DeterministicWallet.derivePrivateKey(parent.priv, index)) + def derivePrivateKey(parent: ExtendedPrivateKey, index: Long): ExtendedPrivateKey = ExtendedPrivateKey(parent.priv.derivePrivateKey(index)) /** * @@ -167,7 +167,7 @@ object DeterministicWallet { * @param index index of the child key * @return the derived public key at the specified index */ - def derivePublicKey(parent: ExtendedPublicKey, index: Long): ExtendedPublicKey = ExtendedPublicKey(bitcoin.DeterministicWallet.derivePublicKey(parent.pub, index)) + def derivePublicKey(parent: ExtendedPublicKey, index: Long): ExtendedPublicKey = ExtendedPublicKey(parent.pub.derivePublicKey(index)) def derivePrivateKey(parent: ExtendedPrivateKey, chain: Seq[Long]): ExtendedPrivateKey = chain.foldLeft(parent)(derivePrivateKey) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala index 2c601435..d16459c4 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala @@ -14,7 +14,9 @@ object Musig2 { * Musig2 secret nonce, that should be treated as a private opaque blob. * This nonce must never be persisted or reused across signing sessions. */ - case class SecretNonce(inner: musig2.SecretNonce) + case class SecretNonce(inner: musig2.SecretNonce) { + def consume[T](f: Array[Byte] => T): Either[Throwable, T] = inner.consume$bitcoin_kmp((bytes: Array[Byte]) => f(bytes)) + } /** * Musig2 public nonce, that must be shared with other participants in the signing session. diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala index ec7e4a9b..29da3c29 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala @@ -116,4 +116,10 @@ class Musig2Spec extends FunSuite { assert(nonce.publicNonce.data == hex"0271efb262c0535e921efacacd30146fa93f193689e4974d5348fa9d909d90000702a049680ef3f6acfb12320297df31d3a634214491cbeebacef5acdf13f8f61cc2") } + test("musig2 nonces can only be used once") { + val sk = PrivateKey(ByteVector.fromValidHex("EEC1CB7D1B7254C5CAB0D9C61AB02E643D464A59FE6C96A7EFE871F07C5AEF54")) + val nonce = Musig2.generateNonceWithCounter(0, sk, Seq(sk.publicKey), None, None) + assert(nonce.secretNonce.consume(a => Crypto.sha256(ByteVector.view(a))).isRight) + assert(nonce.secretNonce.consume(a => Crypto.sha256(ByteVector.view(a))).isLeft) + } } From 02846d98dcc4eaaa9b474455f8bfc70f146fde3e Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 19 May 2026 09:42:23 +0200 Subject: [PATCH 2/5] Add sonatype snapshot repository --- pom.xml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1aad819f..d31d220d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,11 +10,25 @@ - central-snapshots + central_portal https://central.sonatype.com/repository/maven-snapshots + + + central_portal + Sonatype Central Portal (snapshots) + https://central.sonatype.com/repository/maven-snapshots + + false + + + true + + + + 2020-01-01T00:00:00Z UTF-8 @@ -206,7 +220,7 @@ 0.8.0 true - central + central_portal https://central.sonatype.com/repository/maven-snapshots From 5b9d9b2c1383e7fb0493bdf634ee1352c48a8ca5 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 20 May 2026 17:27:30 +0200 Subject: [PATCH 3/5] More efficient wrapper for bitcoin-kmp transaction types Wrapping a bitcoin-kmp transaction instance is much more efficient and benefits from optimizations added in https://github.com/ACINQ/bitcoin-kmp/pull/184, at the cost of having to provide "update" methods instead of using `.copy()`. We also use `.toArrayUnsafe()` to convert scodec byte vectors to byte arrays, which should be more efficient (we use mostly small byte arrays, which would be simply wrapped by scodec and would be passed directly to bitcoin-kmp instead of creating copies). --- .../fr/acinq/bitcoin/scalacompat/Crypto.scala | 38 ++-- .../scalacompat/DeterministicWallet.scala | 6 +- .../bitcoin/scalacompat/KotlinUtils.scala | 18 +- .../scalacompat/LexicographicalOrdering.scala | 20 +-- .../bitcoin/scalacompat/MnemonicCode.scala | 4 +- .../fr/acinq/bitcoin/scalacompat/Musig2.scala | 6 +- .../acinq/bitcoin/scalacompat/Protocol.scala | 12 +- .../fr/acinq/bitcoin/scalacompat/Script.scala | 18 +- .../bitcoin/scalacompat/Transaction.scala | 164 ++++++++++-------- .../bitcoin/scalacompat/SighashSpec.scala | 4 +- 10 files changed, 147 insertions(+), 143 deletions(-) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala index 8b6cd3d8..0b626ab7 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala @@ -40,13 +40,13 @@ object Crypto { def *(that: PrivateKey): PrivateKey = multiply(that) - def isZero: Boolean = priv.value == bitcoin.ByteVector32.Zeroes + def isZero: Boolean = priv.value.equals(fr.acinq.bitcoin.ByteVector32.Zeroes) def isValid: Boolean = priv.isValid def publicKey: PublicKey = PublicKey(priv.publicKey()) - def xOnlyPublicKey(): XonlyPublicKey = XonlyPublicKey(publicKey) + def xOnlyPublicKey(): XonlyPublicKey = XonlyPublicKey(priv.xOnlyPublicKey()) /** * @param prefix Private key prefix @@ -60,7 +60,7 @@ object Crypto { } object PrivateKey { - def apply(data: ByteVector): PrivateKey = PrivateKey(new bitcoin.PrivateKey(data.toArray)) + def apply(data: ByteVector): PrivateKey = PrivateKey(new bitcoin.PrivateKey(data.toArrayUnsafe)) /** * @param data serialized private key in bitcoin format @@ -127,7 +127,7 @@ object Crypto { def fromBin(input: ByteVector, checkValid: Boolean = true): PublicKey = { require(isPubKeyValidLax(input)) require(!checkValid || Crypto.isPubKeyValidStrict(input), "public key is invalid") - PublicKey(new bitcoin.PublicKey(bitcoin.PublicKey.compress(input.toArray))) + PublicKey(new bitcoin.PublicKey(bitcoin.PublicKey.compress(input.toArrayUnsafe))) } } @@ -184,7 +184,7 @@ object Crypto { */ def ecdh(priv: PrivateKey, pub: PublicKey): ByteVector32 = ByteVector32(ByteVector.view(bitcoin.Crypto.ecdh(priv.priv, pub.pub))) - def hmac512(key: ByteVector, data: ByteVector): ByteVector = ByteVector.view(bitcoin.Crypto.hmac512(key.toArray, data.toArray)) + def hmac512(key: ByteVector, data: ByteVector): ByteVector = ByteVector.view(bitcoin.Crypto.hmac512(key.toArrayUnsafe, data.toArrayUnsafe)) def sha256(x: ByteVector): ByteVector32 = ByteVector32(ByteVector.view(bitcoin.Crypto.sha256(x))) @@ -197,7 +197,7 @@ object Crypto { * @param input array of byte * @return the 160 bits BTC hash of input */ - def hash160(input: ByteVector): ByteVector = ByteVector.view(bitcoin.Crypto.hash160(input.toArray)) + def hash160(input: ByteVector): ByteVector = ByteVector.view(bitcoin.Crypto.hash160(input.toArrayUnsafe)) /** * 256 bits bitcoin hash @@ -206,15 +206,15 @@ object Crypto { * @param input array of byte * @return the 256 bits BTC hash of input */ - def hash256(input: ByteVector): ByteVector32 = ByteVector32(ByteVector.view(bitcoin.Crypto.hash256(input.toArray))) + def hash256(input: ByteVector): ByteVector32 = ByteVector32(ByteVector.view(bitcoin.Crypto.hash256(input.toArrayUnsafe))) - def isDERSignature(sig: ByteVector): Boolean = bitcoin.Crypto.isDERSignature(sig.toArray) + def isDERSignature(sig: ByteVector): Boolean = bitcoin.Crypto.isDERSignature(sig.toArrayUnsafe) - def isLowDERSignature(sig: ByteVector): Boolean = bitcoin.Crypto.isLowDERSignature(sig.toArray) + def isLowDERSignature(sig: ByteVector): Boolean = bitcoin.Crypto.isLowDERSignature(sig.toArrayUnsafe) - def checkSignatureEncoding(sig: ByteVector, flags: Int): Boolean = bitcoin.Crypto.checkSignatureEncoding(sig.toArray, flags) + def checkSignatureEncoding(sig: ByteVector, flags: Int): Boolean = bitcoin.Crypto.checkSignatureEncoding(sig.toArrayUnsafe, flags) - def checkPubKeyEncoding(key: ByteVector, flags: Int, sigVersion: Int): Boolean = bitcoin.Crypto.checkPubKeyEncoding(key.toArray, flags, sigVersion) + def checkPubKeyEncoding(key: ByteVector, flags: Int, sigVersion: Int): Boolean = bitcoin.Crypto.checkPubKeyEncoding(key.toArrayUnsafe, flags, sigVersion) /** * @param key serialized public key @@ -232,13 +232,13 @@ object Crypto { * @return true if the key is valid. This check is much more expensive than its lax version since here we check that * the public key is a valid point on the secp256k1 curve */ - def isPubKeyValidStrict(key: ByteVector): Boolean = isPubKeyValidLax(key) && bitcoin.Crypto.isPubKeyValid(key.toArray) + def isPubKeyValidStrict(key: ByteVector): Boolean = isPubKeyValidLax(key) && bitcoin.Crypto.isPubKeyValid(key.toArrayUnsafe) - def isPubKeyCompressedOrUncompressed(key: ByteVector): Boolean = bitcoin.Crypto.isPubKeyCompressedOrUncompressed(key.toArray) + def isPubKeyCompressedOrUncompressed(key: ByteVector): Boolean = bitcoin.Crypto.isPubKeyCompressedOrUncompressed(key.toArrayUnsafe) - def isPubKeyCompressed(key: ByteVector): Boolean = bitcoin.Crypto.isPubKeyCompressed(key.toArray) + def isPubKeyCompressed(key: ByteVector): Boolean = bitcoin.Crypto.isPubKeyCompressed(key.toArrayUnsafe) - def isDefinedHashTypeSignature(sig: ByteVector): Boolean = bitcoin.Crypto.isDefinedHashTypeSignature(sig.toArray) + def isDefinedHashTypeSignature(sig: ByteVector): Boolean = bitcoin.Crypto.isDefinedHashTypeSignature(sig.toArrayUnsafe) /** * @param data data @@ -246,7 +246,7 @@ object Crypto { * @param publicKey public key * @return true is signature is valid for this data with this public key */ - def verifySignature(data: ByteVector, signature: ByteVector64, publicKey: PublicKey): Boolean = bitcoin.Crypto.verifySignature(data.toArray, signature, publicKey.pub) + def verifySignature(data: ByteVector, signature: ByteVector64, publicKey: PublicKey): Boolean = bitcoin.Crypto.verifySignature(data.toArrayUnsafe, signature, publicKey.pub) /** * @param data data @@ -274,7 +274,7 @@ object Crypto { */ def sign(data: Array[Byte], privateKey: PrivateKey): ByteVector64 = bitcoin.Crypto.sign(data, privateKey.priv) - def sign(data: ByteVector, privateKey: PrivateKey): ByteVector64 = sign(data.toArray, privateKey) + def sign(data: ByteVector, privateKey: PrivateKey): ByteVector64 = sign(data.toArrayUnsafe, privateKey) /** * Compute the Schnorr signature of data with private key @@ -296,10 +296,10 @@ object Crypto { * @param message message that was signed * @return a recovered public key */ - def recoverPublicKey(signature: ByteVector64, message: ByteVector, recoveryId: Int): PublicKey = PublicKey(bitcoin.Crypto.recoverPublicKey(signature, message.toArray, recoveryId)) + def recoverPublicKey(signature: ByteVector64, message: ByteVector, recoveryId: Int): PublicKey = PublicKey(bitcoin.Crypto.recoverPublicKey(signature, message.toArrayUnsafe, recoveryId)) def recoverPublicKey(signature: ByteVector64, message: ByteVector): (PublicKey, PublicKey) = { - val p = bitcoin.Crypto.recoverPublicKey(signature, message.toArray) + val p = bitcoin.Crypto.recoverPublicKey(signature, message.toArrayUnsafe) (PublicKey(p.getFirst), PublicKey(p.getSecond)) } diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala index 855d1858..b7e01f8c 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/DeterministicWallet.scala @@ -167,15 +167,15 @@ object DeterministicWallet { * @param index index of the child key * @return the derived public key at the specified index */ - def derivePublicKey(parent: ExtendedPublicKey, index: Long): ExtendedPublicKey = ExtendedPublicKey(parent.pub.derivePublicKey(index)) + def derivePublicKey(parent: ExtendedPublicKey, index: Long): ExtendedPublicKey = parent.derivePublicKey(index) - def derivePrivateKey(parent: ExtendedPrivateKey, chain: Seq[Long]): ExtendedPrivateKey = chain.foldLeft(parent)(derivePrivateKey) + def derivePrivateKey(parent: ExtendedPrivateKey, chain: Seq[Long]): ExtendedPrivateKey = parent.derivePrivateKey(chain) def derivePrivateKey(parent: ExtendedPrivateKey, keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(parent, keyPath.path) def derivePrivateKey(parent: ExtendedPrivateKey, path: String): ExtendedPrivateKey = derivePrivateKey(parent, KeyPath(path)) - def derivePublicKey(parent: ExtendedPublicKey, chain: Seq[Long]): ExtendedPublicKey = chain.foldLeft(parent)(derivePublicKey) + def derivePublicKey(parent: ExtendedPublicKey, chain: Seq[Long]): ExtendedPublicKey = parent.derivePublicKey(chain) def derivePublicKey(parent: ExtendedPublicKey, keyPath: KeyPath): ExtendedPublicKey = derivePublicKey(parent, keyPath.path) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala index 607c8755..17acfaf2 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala @@ -9,17 +9,17 @@ import scala.jdk.CollectionConverters.{ListHasAsScala, SeqHasAsJava} object KotlinUtils { - implicit def kmp2scala(input: bitcoin.ByteVector32): ByteVector32 = ByteVector32(ByteVector(input.toByteArray)) + implicit def kmp2scala(input: bitcoin.ByteVector32): ByteVector32 = ByteVector32(ByteVector.view(input.toByteArray)) - implicit def scala2kmp(input: ByteVector32): bitcoin.ByteVector32 = new bitcoin.ByteVector32(input.toArray) + implicit def scala2kmp(input: ByteVector32): bitcoin.ByteVector32 = new bitcoin.ByteVector32(input.toArrayUnsafe) - implicit def kmp2scala(input: bitcoin.ByteVector64): ByteVector64 = ByteVector64(ByteVector(input.toByteArray)) + implicit def kmp2scala(input: bitcoin.ByteVector64): ByteVector64 = ByteVector64(ByteVector.view(input.toByteArray)) - implicit def scala2kmp(input: ByteVector64): bitcoin.ByteVector64 = new bitcoin.ByteVector64(input.toArray) + implicit def scala2kmp(input: ByteVector64): bitcoin.ByteVector64 = new bitcoin.ByteVector64(input.toArrayUnsafe) - implicit def kmp2scala(input: bitcoin.ByteVector): ByteVector = ByteVector(input.toByteArray) + implicit def kmp2scala(input: bitcoin.ByteVector): ByteVector = ByteVector.view(input.toByteArray) - implicit def scala2kmp(input: ByteVector): bitcoin.ByteVector = new bitcoin.ByteVector(input.toArray) + implicit def scala2kmp(input: ByteVector): bitcoin.ByteVector = new bitcoin.ByteVector(input.toArrayUnsafe) implicit def kmp2scala(input: bitcoin.TxId): TxId = TxId(input.value) @@ -84,11 +84,11 @@ object KotlinUtils { implicit def kmp2scala(input: bitcoin.TxOut): TxOut = TxOut(input.amount, input.publicKeyScript) - implicit def scala2kmp(input: TxOut): bitcoin.TxOut = new bitcoin.TxOut(input.amount, input.publicKeyScript) + implicit def scala2kmp(input: TxOut): bitcoin.TxOut = input.kmp - implicit def kmp2scala(input: bitcoin.Transaction): Transaction = Transaction(input.version, input.txIn.asScala.toList.map(kmp2scala), input.txOut.asScala.toList.map(kmp2scala), input.lockTime) + implicit def kmp2scala(input: bitcoin.Transaction): Transaction = Transaction(input) - implicit def scala2kmp(input: Transaction): bitcoin.Transaction = new bitcoin.Transaction(input.version, input.txIn.map(scala2kmp).asJava, input.txOut.map(scala2kmp).asJava, input.lockTime) + implicit def scala2kmp(input: Transaction): bitcoin.Transaction = input.inner implicit def kmp2scala(input: bitcoin.PrivateKey): PrivateKey = PrivateKey(input) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala index c3855689..cbf981ca 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala @@ -9,23 +9,7 @@ import scala.annotation.tailrec * see https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki */ object LexicographicalOrdering { - @tailrec - def isLessThan(a: Seq[Byte], b: Seq[Byte]): Boolean = { - if (a.isEmpty && b.isEmpty) false - else if (a.isEmpty) true - else if (b.isEmpty) false - else if (a.head == b.head) isLessThan(a.tail, b.tail) - else (a.head & 0xff) < (b.head & 0xff) - } - - @tailrec - def isLessThan(a: ByteVector, b: ByteVector): Boolean = { - if (a.isEmpty && b.isEmpty) false - else if (a.isEmpty) true - else if (b.isEmpty) false - else if (a.head == b.head) isLessThan(a.tail, b.tail) - else (a.head & 0xff) < (b.head & 0xff) - } + def isLessThan(a: ByteVector, b: ByteVector): Boolean = fr.acinq.bitcoin.LexicographicalOrdering.isLessThan(a.toArrayUnsafe, b.toArrayUnsafe) def isLessThan(a: OutPoint, b: OutPoint): Boolean = { if (a.txid == b.txid) a.index < b.index @@ -43,5 +27,5 @@ object LexicographicalOrdering { * @param tx input transaction * @return the input tx with inputs and outputs sorted in lexicographical order */ - def sort(tx: Transaction): Transaction = tx.copy(txIn = tx.txIn.sortWith(isLessThan), txOut = tx.txOut.sortWith(isLessThan)) + def sort(tx: Transaction): Transaction = Transaction(fr.acinq.bitcoin.LexicographicalOrdering.sort(tx.inner)) } diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/MnemonicCode.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/MnemonicCode.scala index d20be732..3ab7dbeb 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/MnemonicCode.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/MnemonicCode.scala @@ -15,7 +15,7 @@ object MnemonicCode { * @param entropy input entropy * @return a list of mnemonic words that encodes the input entropy */ - def toMnemonics(entropy: ByteVector): List[String] = bitcoin.MnemonicCode.toMnemonics(entropy.toArray).asScala.toList + def toMnemonics(entropy: ByteVector): List[String] = bitcoin.MnemonicCode.toMnemonics(entropy.toArrayUnsafe).asScala.toList /** * BIP39 entropy encoding. @@ -24,7 +24,7 @@ object MnemonicCode { * @param wordlist word list (must be 2048 words long) * @return a list of mnemonic words that encodes the input entropy */ - def toMnemonics(entropy: ByteVector, wordlist: Seq[String]): List[String] = bitcoin.MnemonicCode.toMnemonics(entropy.toArray, wordlist.asJava).asScala.toList + def toMnemonics(entropy: ByteVector, wordlist: Seq[String]): List[String] = bitcoin.MnemonicCode.toMnemonics(entropy.toArrayUnsafe, wordlist.asJava).asScala.toList /** * Verify that a mnemonic seed is valid using default BIP39 word list. diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala index d16459c4..e724bb4a 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala @@ -75,7 +75,7 @@ object Musig2 { * @param scriptTree_opt tapscript tree of the taproot input, if it has script paths. */ def signTaprootInput(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], publicKeys: Seq[PublicKey], secretNonce: SecretNonce, publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Either[Throwable, ByteVector32] = { - musig2.Musig2.signTaprootInput(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, secretNonce.inner, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala) + musig2.Musig2.signTaprootInput(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, secretNonce.inner, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArrayUnsafe)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala) } /** @@ -93,7 +93,7 @@ object Musig2 { * @return true if the partial signature is valid. */ def verifyTaprootSignature(partialSig: ByteVector32, nonce: IndividualNonce, publicKey: PublicKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Boolean = { - musig2.Musig2.verify(partialSig, new musig2.IndividualNonce(nonce.data.toArray), publicKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull) + musig2.Musig2.verify(partialSig, new musig2.IndividualNonce(nonce.data.toArrayUnsafe), publicKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArrayUnsafe)).asJava, scriptTree_opt.map(scala2kmp).orNull) } /** @@ -108,7 +108,7 @@ object Musig2 { * @param scriptTree_opt tapscript tree of the taproot input, if it has script paths. */ def aggregateTaprootSignatures(partialSigs: Seq[ByteVector32], tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Either[Throwable, ByteVector64] = { - musig2.Musig2.aggregateTaprootSignatures(partialSigs.map(scala2kmp).asJava, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala) + musig2.Musig2.aggregateTaprootSignatures(partialSigs.map(scala2kmp).asJava, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArrayUnsafe)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala) } } diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Protocol.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Protocol.scala index 766002f8..92b6f985 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Protocol.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Protocol.scala @@ -61,7 +61,7 @@ object Protocol { buffer.getShort & 0xFFFF } - def writeUInt16(input: Int, out: OutputStream, order: ByteOrder = ByteOrder.LITTLE_ENDIAN): Unit = out.write(writeUInt16(input, order).toArray) + def writeUInt16(input: Int, out: OutputStream, order: ByteOrder = ByteOrder.LITTLE_ENDIAN): Unit = out.write(writeUInt16(input, order).toArrayUnsafe) def writeUInt16(input: Int, order: ByteOrder): ByteVector = { val bin = new Array[Byte](2) @@ -85,7 +85,7 @@ object Protocol { input.toLong(signed = false, ByteOrdering.fromJava(order)) } - def writeUInt32(input: Long, out: OutputStream, order: ByteOrder = ByteOrder.LITTLE_ENDIAN): Unit = out.write(writeUInt32(input, order).toArray) + def writeUInt32(input: Long, out: OutputStream, order: ByteOrder = ByteOrder.LITTLE_ENDIAN): Unit = out.write(writeUInt32(input, order).toArrayUnsafe) def writeUInt32(input: Long, order: ByteOrder): ByteVector = { val bin = new Array[Byte](4) @@ -107,7 +107,7 @@ object Protocol { buffer.getLong() } - def writeUInt64(input: Long, out: OutputStream, order: ByteOrder = ByteOrder.LITTLE_ENDIAN): Unit = out.write(writeUInt64(input, order).toArray) + def writeUInt64(input: Long, out: OutputStream, order: ByteOrder = ByteOrder.LITTLE_ENDIAN): Unit = out.write(writeUInt64(input, order).toArrayUnsafe) def writeUInt64(input: Long, order: ByteOrder): ByteVector = { val bin = new Array[Byte](8) @@ -156,11 +156,11 @@ object Protocol { def writeBytes(input: Array[Byte], out: OutputStream): Unit = out.write(input) - def writeBytes(input: ByteVector, out: OutputStream): Unit = out.write(input.toArray) + def writeBytes(input: ByteVector, out: OutputStream): Unit = out.write(input.toArrayUnsafe) def varstring(input: InputStream): String = { val length = varint(input) - new String(bytes(input, length).toArray, "UTF-8") + new String(bytes(input, length).toArrayUnsafe, "UTF-8") } def writeVarstring(input: String, out: OutputStream): Unit = { @@ -234,7 +234,7 @@ trait BtcSerializer[T] { * @param in message binary data in hex format * @return a deserialized message of type T */ - def read(in: String, protocolVersion: Long): T = read(ByteVector.fromValidHex(in).toArray, protocolVersion) + def read(in: String, protocolVersion: Long): T = read(ByteVector.fromValidHex(in).toArrayUnsafe, protocolVersion) def read(in: String): T = read(in, PROTOCOL_VERSION) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala index 41b08bc3..215230f2 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala @@ -11,7 +11,7 @@ object Script { import fr.acinq.bitcoin.ScriptFlags._ - def parse(blob: ByteVector): List[ScriptElt] = parse(blob.toArray) + def parse(blob: ByteVector): List[ScriptElt] = parse(blob.toArrayUnsafe) def parse(blob: Array[Byte]): List[ScriptElt] = bitcoin.Script.parse(blob).asScala.toList.map(kmp2scala) @@ -19,7 +19,7 @@ object Script { def encodeNumber(value: Long): ByteVector = ByteVector.view(bitcoin.Script.encodeNumber(value).toByteArray) - def decodeNumber(input: ByteVector, checkMinimalEncoding: Boolean, maximumSize: Int = 4): Long = bitcoin.Script.decodeNumber(input.toArray, checkMinimalEncoding, maximumSize) + def decodeNumber(input: ByteVector, checkMinimalEncoding: Boolean, maximumSize: Int = 4): Long = bitcoin.Script.decodeNumber(input.toArrayUnsafe, checkMinimalEncoding, maximumSize) def isSimpleValue(op: ScriptElt): Boolean = bitcoin.Script.isSimpleValue(op) @@ -27,7 +27,7 @@ object Script { def isPushOnly(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPushOnly(script.map(scala2kmp).asJava) - def isPayToScript(script: ByteVector): Boolean = bitcoin.Script.isPayToScript(script.toArray) + def isPayToScript(script: ByteVector): Boolean = bitcoin.Script.isPayToScript(script.toArrayUnsafe) def isNativeWitnessScript(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isNativeWitnessScript(script.map(scala2kmp).asJava) @@ -52,7 +52,7 @@ object Script { * @param inputIndex 0-based index of the tx input that is being processed */ case class Context(tx: Transaction, inputIndex: Int, amount: Satoshi, prevouts: List[TxOut] = Nil) { - require(inputIndex >= 0 && inputIndex < tx.txIn.length, "invalid input index") + require(inputIndex >= 0 && inputIndex < tx.inner.txIn.size(), "invalid input index") } /** @@ -65,7 +65,7 @@ object Script { private val runner = new bitcoin.Script.Runner(new bitcoin.Script.Context(context.tx, context.inputIndex, context.amount, context.prevouts.map(scala2kmp).asJava), scriptFlag) - def verifyWitnessProgram(witness: ScriptWitness, witnessVersion: Long, program: ByteVector, isP2sh: Boolean = false): Unit = runner.verifyWitnessProgram(witness, witnessVersion, program.toArray, isP2sh) + def verifyWitnessProgram(witness: ScriptWitness, witnessVersion: Long, program: ByteVector, isP2sh: Boolean = false): Unit = runner.verifyWitnessProgram(witness, witnessVersion, program.toArrayUnsafe, isP2sh) def verifyScripts(scriptSig: ByteVector, scriptPubKey: ByteVector): Boolean = verifyScripts(scriptSig, scriptPubKey, ScriptWitness.empty) @@ -106,7 +106,7 @@ object Script { * @param pubKeyHash public key hash * @return a pay-to-public-key-hash script */ - def pay2pkh(pubKeyHash: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2pkh(pubKeyHash.toArray).asScala.map(kmp2scala).toList + def pay2pkh(pubKeyHash: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2pkh(pubKeyHash.toArrayUnsafe).asScala.map(kmp2scala).toList /** * @param pubKey public key @@ -126,7 +126,7 @@ object Script { * @param script bitcoin script * @return a pay-to-script script */ - def pay2sh(script: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2sh(script.toArray).asScala.map(kmp2scala).toList + def pay2sh(script: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2sh(script.toArrayUnsafe).asScala.map(kmp2scala).toList def isPay2sh(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPay2sh(script.map(scala2kmp).asJava) @@ -140,7 +140,7 @@ object Script { * @param script bitcoin script * @return a pay-to-witness-script script */ - def pay2wsh(script: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2wsh(script.toArray).asScala.map(kmp2scala).toList + def pay2wsh(script: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2wsh(script.toArrayUnsafe).asScala.map(kmp2scala).toList def isPay2wsh(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPay2wsh(script.map(scala2kmp).asJava) @@ -148,7 +148,7 @@ object Script { * @param pubKeyHash public key hash * @return a pay-to-witness-public-key-hash script */ - def pay2wpkh(pubKeyHash: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2wpkh(pubKeyHash.toArray).asScala.map(kmp2scala).toList + def pay2wpkh(pubKeyHash: ByteVector): Seq[ScriptElt] = bitcoin.Script.pay2wpkh(pubKeyHash.toArrayUnsafe).asScala.map(kmp2scala).toList /** * @param pubKey public key diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala index 1db20523..625d841d 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala @@ -1,13 +1,13 @@ package fr.acinq.bitcoin.scalacompat -import fr.acinq.bitcoin +import fr.acinq.{bitcoin => bitcoinkmp} import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.bitcoin.scalacompat.Protocol._ import scodec.bits.ByteVector import java.io.{InputStream, OutputStream} -import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} +import scala.jdk.CollectionConverters.{ListHasAsScala, MapHasAsJava, SeqHasAsJava} /** * This is the double hash of a transaction serialized without witness data. @@ -42,9 +42,9 @@ object OutPoint extends BtcSerializer[OutPoint] { def apply(txid: TxId, index: Long): OutPoint = OutPoint(TxHash(txid), index) - override def read(input: InputStream, protocolVersion: Long): OutPoint = kmp2scala(fr.acinq.bitcoin.OutPoint.read(InputStreamWrapper(input), protocolVersion)) + override def read(input: InputStream, protocolVersion: Long): OutPoint = kmp2scala(bitcoinkmp.OutPoint.read(InputStreamWrapper(input), protocolVersion)) - override def write(input: OutPoint, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.OutPoint.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) + override def write(input: OutPoint, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.OutPoint.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) def isCoinbase(input: OutPoint): Boolean = scala2kmp(input).isCoinbase @@ -71,12 +71,12 @@ case class OutPoint(hash: TxHash, index: Long) extends BtcSerializable[OutPoint] object TxIn extends BtcSerializer[TxIn] { def apply(outPoint: OutPoint, signatureScript: Seq[ScriptElt], sequence: Long): TxIn = new TxIn(outPoint, Script.write(signatureScript), sequence) - override def read(input: InputStream, protocolVersion: Long): TxIn = kmp2scala(fr.acinq.bitcoin.TxIn.read(InputStreamWrapper(input), protocolVersion)) + override def read(input: InputStream, protocolVersion: Long): TxIn = kmp2scala(bitcoinkmp.TxIn.read(InputStreamWrapper(input), protocolVersion)) - override def write(input: TxIn, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.TxIn.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) + override def write(input: TxIn, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.TxIn.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) override def validate(input: TxIn): Unit = { - require(input.signatureScript.length <= bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE, s"signature script is ${input.signatureScript.length} bytes, limit is ${bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") + require(input.signatureScript.length <= bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE, s"signature script is ${input.signatureScript.length} bytes, limit is ${bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") } def coinbase(script: ByteVector): TxIn = { @@ -86,7 +86,7 @@ object TxIn extends BtcSerializer[TxIn] { def coinbase(script: Seq[ScriptElt]): TxIn = coinbase(Script.write(script)) - val SEQUENCE_FINAL: Long = fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL + val SEQUENCE_FINAL: Long = bitcoinkmp.TxIn.SEQUENCE_FINAL } /** @@ -99,7 +99,7 @@ object TxIn extends BtcSerializer[TxIn] { * @param witness Transaction witness (i.e. what is in sig script for standard transactions). */ case class TxIn(outPoint: OutPoint, signatureScript: ByteVector, sequence: Long, witness: ScriptWitness = ScriptWitness.empty) extends BtcSerializable[TxIn] { - def isFinal: Boolean = sequence == bitcoin.TxIn.SEQUENCE_FINAL + def isFinal: Boolean = sequence == bitcoinkmp.TxIn.SEQUENCE_FINAL def hasWitness: Boolean = witness.isNotNull def weight(): Int = scala2kmp(this).weight() @@ -109,15 +109,15 @@ case class TxIn(outPoint: OutPoint, signatureScript: ByteVector, sequence: Long, object TxOut extends BtcSerializer[TxOut] { def apply(amount: Satoshi, publicKeyScript: Seq[ScriptElt]): TxOut = new TxOut(amount, Script.write(publicKeyScript)) - override def read(input: InputStream, protocolVersion: Long): TxOut = kmp2scala(fr.acinq.bitcoin.TxOut.read(InputStreamWrapper(input), protocolVersion)) + override def read(input: InputStream, protocolVersion: Long): TxOut = kmp2scala(bitcoinkmp.TxOut.read(InputStreamWrapper(input), protocolVersion)) - override def write(input: TxOut, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.TxOut.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) + override def write(input: TxOut, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.TxOut.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) override def validate(input: TxOut): Unit = { import input._ require(amount.toLong >= 0, s"invalid txout amount: $amount") require(amount.toLong <= BtcAmount.MaxMoney, s"invalid txout amount: $amount") - require(publicKeyScript.length < bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE, s"public key script is ${publicKeyScript.length} bytes, limit is ${bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") + require(publicKeyScript.length < bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE, s"public key script is ${publicKeyScript.length} bytes, limit is ${bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") } } @@ -128,7 +128,10 @@ object TxOut extends BtcSerializer[TxOut] { * @param publicKeyScript public key script which sets the conditions for spending this output */ case class TxOut(amount: Satoshi, publicKeyScript: ByteVector) extends BtcSerializable[TxOut] { - def weight(): Int = scala2kmp(this).weight() + // TxOut will almost always need to be converted to its bitcoin-kmp counterpart so we precompute this conversion here + val kmp: bitcoinkmp.TxOut = new bitcoinkmp.TxOut(amount, publicKeyScript.toArrayUnsafe) + + def weight(): Int = kmp.weight() override def serializer: BtcSerializer[TxOut] = TxOut } @@ -136,9 +139,9 @@ case class TxOut(amount: Satoshi, publicKeyScript: ByteVector) extends BtcSerial object ScriptWitness extends BtcSerializer[ScriptWitness] { val empty: ScriptWitness = ScriptWitness(Seq.empty[ByteVector]) - override def write(t: ScriptWitness, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.ScriptWitness.write(scala2kmp(t), OutputStreamWrapper(out), protocolVersion) + override def write(t: ScriptWitness, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.ScriptWitness.write(scala2kmp(t), OutputStreamWrapper(out), protocolVersion) - override def read(in: InputStream, protocolVersion: Long): ScriptWitness = kmp2scala(fr.acinq.bitcoin.ScriptWitness.read(InputStreamWrapper(in), protocolVersion)) + override def read(in: InputStream, protocolVersion: Long): ScriptWitness = kmp2scala(bitcoinkmp.ScriptWitness.read(InputStreamWrapper(in), protocolVersion)) } /** @@ -156,33 +159,45 @@ case class ScriptWitness(stack: Seq[ByteVector]) extends BtcSerializable[ScriptW } object Transaction extends BtcSerializer[Transaction] { + /** + * + * @param version Transaction data format version + * @param txIn Transaction inputs + * @param txOut Transaction outputs + * @param lockTime The block number or timestamp at which this transaction is locked + * @return a new transaction + */ + def apply(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTime: Long): Transaction = Transaction( + new bitcoinkmp.Transaction(version, txIn.map(scala2kmp).asJava, txOut.map(scala2kmp).asJava, lockTime) + ) + /** * * @param version protocol version (and NOT transaction version !) * @return true if protocol version specifies that witness data is to be serialized */ - def serializeTxWitness(version: Long): Boolean = (version & bitcoin.Transaction.SERIALIZE_TRANSACTION_NO_WITNESS) == 0 + def serializeTxWitness(version: Long): Boolean = bitcoinkmp.Transaction.serializeTxWitness(version) override def read(input: InputStream, protocolVersion: Long): Transaction = { - val tx = fr.acinq.bitcoin.Transaction.read(InputStreamWrapper(input), protocolVersion) - tx + val tx = bitcoinkmp.Transaction.read(InputStreamWrapper(input), protocolVersion) + Transaction(tx) } override def write(tx: Transaction, out: OutputStream, protocolVersion: Long): Unit = { - fr.acinq.bitcoin.Transaction.write(tx, OutputStreamWrapper(out), protocolVersion) + bitcoinkmp.Transaction.write(tx.inner, OutputStreamWrapper(out), protocolVersion) } override def validate(input: Transaction): Unit = { - fr.acinq.bitcoin.Transaction.validate(input) + bitcoinkmp.Transaction.validate(input.inner) } - def baseSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = fr.acinq.bitcoin.Transaction.baseSize(scala2kmp(tx), protocolVersion) + def baseSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = tx.inner.baseSize(protocolVersion) - def totalSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = fr.acinq.bitcoin.Transaction.totalSize(scala2kmp(tx), protocolVersion) + def totalSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = tx.inner.baseSize(protocolVersion) - def weight(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = totalSize(tx, protocolVersion) + 3 * baseSize(tx, protocolVersion) + def weight(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = tx.inner.weight(protocolVersion) - def isCoinbase(input: Transaction): Boolean = input.txIn.size == 1 && OutPoint.isCoinbase(input.txIn.head.outPoint) + def isCoinbase(input: Transaction): Boolean = input.inner.isCoinbase /** * prepare a transaction for signing a specific input @@ -207,7 +222,7 @@ object Transaction extends BtcSerializer[Transaction] { * @return a hash which can be used to sign the referenced tx input */ def hashForSigning(tx: Transaction, inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int): ByteVector32 = { - ByteVector32(ByteVector.view(fr.acinq.bitcoin.Transaction.hashForSigning(tx, inputIndex, previousOutputScript.toArray, sighashType))) + ByteVector32(ByteVector.view(bitcoinkmp.Transaction.hashForSigning(tx, inputIndex, previousOutputScript.toArrayUnsafe, sighashType))) } /** @@ -308,7 +323,7 @@ object Transaction extends BtcSerializer[Transaction] { * @param sighashType signature hash type, which will be appended to the signature * @return an ECDSA signature in the format used in transaction witnesses: DER encoded followed by a sighash byte */ - def encodeWitnessEcdsaSig(sig: ByteVector64, sighashType: Int): ByteVector = ByteVector.view(fr.acinq.bitcoin.Transaction.encodeWitnessEcdsaSig(sig, sighashType)) + def encodeWitnessEcdsaSig(sig: ByteVector64, sighashType: Int): ByteVector = ByteVector.view(bitcoinkmp.Transaction.encodeWitnessEcdsaSig(sig, sighashType)) /** * Sign a taproot tx input, using the internal key path. @@ -350,87 +365,92 @@ object Transaction extends BtcSerializer[Transaction] { } /** - * Transaction + * Instead of using a pure Scala case class, we simply wrap an instance of bitcoin-kmp transaction. + * This is done because kmp<->scala conversion is expensive and to benefit from optmisations (such as + * pre-computed hashes used to sign transactions, see https://github.com/ACINQ/bitcoin-kmp/pull/184) + * + * But this also means that we cannot use `.copy()` to update inputs or outputs for example, and must provide + * helper methods instead. * - * @param version Transaction data format version - * @param txIn Transaction inputs - * @param txOut Transaction outputs - * @param lockTime The block number or timestamp at which this transaction is locked */ -case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTime: Long) extends BtcSerializable[Transaction] { +case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Transaction] { + val version: Long = inner.version + val lockTime: Long = inner.lockTime // standard transaction hash, used to identify transactions (in transactions outputs for example) - lazy val hash: TxHash = TxHash(Crypto.hash256(Transaction.write(this, bitcoin.Transaction.SERIALIZE_TRANSACTION_NO_WITNESS))) - lazy val txid: TxId = TxId(hash) + lazy val hash: TxHash = kmp2scala(inner.hash) + lazy val txid: TxId = kmp2scala(inner.txid) + // witness transaction hash that includes witness data. used to compute the witness commitment included in the coinbase // transaction of segwit blocks - lazy val whash: ByteVector32 = Crypto.hash256(Transaction.write(this)) + lazy val whash: ByteVector32 = Crypto.hash256(bin) lazy val wtxid: ByteVector32 = whash.reverse lazy val bin: ByteVector = Transaction.write(this) // this is much easier to use than Scala's default toString override def toString: String = bin.toHex + lazy val txOut: Seq[TxOut] = inner.txOut.asScala.map(kmp2scala).toSeq + + lazy val txIn: Seq[TxIn] = inner.txIn.asScala.map(kmp2scala).toSeq + + def updateInputs(txIn: Seq[TxIn]) : Transaction = Transaction(inner.updateInputs(txIn.map(scala2kmp).asJava)) + + def updateOutputs(txOut: Seq[TxOut]) : Transaction = Transaction(inner.updateOutputs(txOut.map(scala2kmp).asJava)) + + def updateInputsAndOutputs(txIn: Seq[TxIn], txOut: Seq[TxOut]) : Transaction = Transaction(inner.copy(inner.version, txIn.map(scala2kmp).asJava, txOut.map(scala2kmp).asJava, inner.lockTime)) + + def updateLockTime(lockTime: Long): Transaction = Transaction(inner.copy(inner.version, inner.txIn, inner.txOut, lockTime)) + /** * * @param blockHeight current block height * @param blockTime current block time * @return true if the transaction is final */ - def isFinal(blockHeight: Long, blockTime: Long): Boolean = lockTime match { - case 0 => true - case value if value < bitcoin.Transaction.LOCKTIME_THRESHOLD && value < blockHeight => true - case value if value >= bitcoin.Transaction.LOCKTIME_THRESHOLD && value < blockTime => true - case _ if txIn.exists(!_.isFinal) => false - case _ => true - } + def isFinal(blockHeight: Long, blockTime: Long): Boolean = inner.isFinal(blockHeight, blockTime) /** * * @param i index of the tx input to update * @param sigScript new signature script - * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replace by sigscript + * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replaced by sigScript */ - def updateSigScript(i: Int, sigScript: ByteVector): Transaction = this.copy(txIn = txIn.updated(i, txIn(i).copy(signatureScript = sigScript))) + def updateSigScript(i: Int, sigScript: ByteVector): Transaction = Transaction(inner.updateSigScript(i, sigScript.toArrayUnsafe)) /** * * @param i index of the tx input to update * @param sigScript new signature script - * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replace by sigscript + * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replaced by sigScript */ - def updateSigScript(i: Int, sigScript: Seq[ScriptElt]): Transaction = updateSigScript(i, Script.write(sigScript)) + def updateSigScript(i: Int, sigScript: Seq[ScriptElt]): Transaction = Transaction(inner.updateSigScript(i, sigScript.map(scala2kmp).asJava)) - def updateWitness(i: Int, witness: ScriptWitness): Transaction = this.copy(txIn = txIn.updated(i, txIn(i).copy(witness = witness))) + def updateWitness(i: Int, witness: ScriptWitness): Transaction = Transaction(inner.updateWitness(i, scala2kmp(witness))) - def updateWitnesses(witnesses: Seq[ScriptWitness]): Transaction = { - require(witnesses.length == txIn.length) - witnesses.zipWithIndex.foldLeft(this) { - case (tx, (witness, index)) => tx.updateWitness(index, witness) - } - } + def updateWitnesses(witnesses: Seq[ScriptWitness]): Transaction = Transaction(inner.updateWitnesses(witnesses.map(scala2kmp).asJava)) - def hasWitness: Boolean = txIn.exists(_.hasWitness) + def hasWitness: Boolean = inner.getHasWitness /** * * @param input input to add the tx * @return a new transaction which includes the newly added input */ - def addInput(input: TxIn): Transaction = this.copy(txIn = this.txIn :+ input) + def addInput(input: TxIn): Transaction = this.copy(inner = inner.addInput(scala2kmp(input))) /** * * @param output output to add to the tx * @return a new transaction which includes the newly added output */ - def addOutput(output: TxOut): Transaction = this.copy(txOut = this.txOut :+ output) + def addOutput(output: TxOut): Transaction = this.copy(inner = inner.addOutput(scala2kmp(output))) - def baseSize(protocolVersion: Long = PROTOCOL_VERSION): Int = Transaction.baseSize(this, protocolVersion) + def baseSize(protocolVersion: Long = PROTOCOL_VERSION): Int = inner.baseSize(protocolVersion) - def totalSize(protocolVersion: Long = PROTOCOL_VERSION): Int = Transaction.totalSize(this, protocolVersion) + def totalSize(protocolVersion: Long = PROTOCOL_VERSION): Int = inner.totalSize(protocolVersion) - def weight(protocolVersion: Long = PROTOCOL_VERSION): Int = Transaction.weight(this, protocolVersion) + def weight(protocolVersion: Long = PROTOCOL_VERSION): Int = inner.weight(protocolVersion) /** * prepare a transaction for signing a specific input @@ -441,7 +461,7 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return a new transaction with proper inputs and outputs according to SIGHASH_TYPE rules */ def prepareForSigning(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int): Transaction = { - scala2kmp(this).prepareForSigning(inputIndex, previousOutputScript.toArray, sighashType) + Transaction(inner.prepareForSigning(inputIndex, previousOutputScript.toArrayUnsafe, sighashType)) } /** @@ -454,7 +474,7 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return a hash which can be used to sign the referenced tx input */ def hashForSigning(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int, amount: Satoshi, signatureVersion: Int): ByteVector32 = { - ByteVector32(ByteVector.view(scala2kmp(this).hashForSigning(inputIndex, previousOutputScript.toArray, sighashType, amount, signatureVersion))) + ByteVector32(ByteVector.view(inner.hashForSigning(inputIndex, previousOutputScript.toArrayUnsafe, sighashType, amount, signatureVersion))) } /** @@ -478,17 +498,17 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @param annex_opt (optional) taproot annex */ def hashForSigningSchnorr(inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, sigVersion: Int, tapleaf_opt: Option[ByteVector32] = None, annex_opt: Option[ByteVector] = None): ByteVector32 = { - scala2kmp(this).hashForSigningSchnorr(inputIndex, inputs.map(scala2kmp).asJava, sighashType, sigVersion, tapleaf_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, null) + inner.hashForSigningSchnorr(inputIndex, inputs.map(scala2kmp).asJava, sighashType, sigVersion, tapleaf_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, null) } /** Use this function when spending a taproot key path. */ def hashForSigningTaprootKeyPath(inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, annex_opt: Option[ByteVector] = None): ByteVector32 = { - scala2kmp(this).hashForSigningTaprootKeyPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, annex_opt.map(scala2kmp).orNull) + inner.hashForSigningTaprootKeyPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, annex_opt.map(scala2kmp).orNull) } /** Use this function when spending a taproot script path. */ def hashForSigningTaprootScriptPath(inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None): ByteVector32 = { - scala2kmp(this).hashForSigningTaprootScriptPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, scala2kmp(tapleaf), annex_opt.map(scala2kmp).orNull) + inner.hashForSigningTaprootScriptPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, scala2kmp(tapleaf), annex_opt.map(scala2kmp).orNull) } /** @@ -503,7 +523,7 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return the encoded signature of this tx for this specific tx input in compact 64 bytes format */ def signInputCompact(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector64 = { - scala2kmp(this).signInputCompact(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv) + inner.signInputCompact(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv) } /** @@ -518,7 +538,7 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return the encoded signature of this tx for this specific tx input in compact 64 bytes format */ def signInputCompact(inputIndex: Int, previousOutputScript: Seq[ScriptElt], sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector64 = - signInputCompact(inputIndex, Script.write(previousOutputScript), sighashType, amount, signatureVersion, privateKey) + inner.signInputCompact(inputIndex, previousOutputScript.map(scala2kmp).asJava, sighashType, amount, signatureVersion, privateKey.priv) /** @@ -533,13 +553,13 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return the encoded signature of this tx for this specific tx input */ def signInput(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector = { - ByteVector.view(scala2kmp(this).signInput(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv)) + ByteVector.view(inner.signInput(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv)) } /** * sign a tx input * - * @param inputIndex index of the tx input that is being processed + * @param inputIndex index of the input that is being processed * @param previousOutputScript public key script of the output claimed by this tx input * @param sighashType signature hash type, which will be appended to the signature * @param amount amount of the output claimed by this tx input @@ -561,7 +581,7 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return the schnorr signature of this tx for this specific tx input. */ def signInputTaprootKeyPath(privateKey: PrivateKey, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, scriptTree_opt: Option[ScriptTree], annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = { - scala2kmp(this).signInputTaprootKeyPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) + inner.signInputTaprootKeyPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) } /** @@ -575,15 +595,15 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi * @return the schnorr signature of this tx for this specific tx input and the given script leaf. */ def signInputTaprootScriptPath(privateKey: PrivateKey, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = { - scala2kmp(this).signInputTaprootScriptPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, tapleaf, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) + inner.signInputTaprootScriptPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, tapleaf, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) } def correctlySpends(previousOutputs: Map[OutPoint, TxOut], scriptFlags: Int): Unit = { - scala2kmp(this).correctlySpends(previousOutputs.map { case (o, t) => scala2kmp(o) -> scala2kmp(t) }.asJava, scriptFlags) + inner.correctlySpends(previousOutputs.map { case (o, t) => scala2kmp(o) -> scala2kmp(t) }.asJava, scriptFlags) } def correctlySpends(inputs: Seq[Transaction], scriptFlags: Int): Unit = { - scala2kmp(this).correctlySpends(inputs.map(scala2kmp).asJava, scriptFlags) + inner.correctlySpends(inputs.map(scala2kmp).asJava, scriptFlags) } override def serializer: BtcSerializer[Transaction] = Transaction diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala index 774b9e48..c7ad06e5 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala @@ -45,7 +45,7 @@ class SighashSpec extends FunSuite { tx2.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // but I cannot change the tx output - val tx3 = tx2.copy(txOut = tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc))) + val tx3 = Transaction(tx2.version, tx2.txIn, tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc)), tx2.lockTime) intercept[RuntimeException] { tx3.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -84,7 +84,7 @@ class SighashSpec extends FunSuite { tx2.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // but I cannot change the tx output - val tx3 = tx2.copy(txOut = tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc))) + val tx3 = Transaction(tx2.version, tx2.txIn, tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc)), tx2.lockTime) intercept[RuntimeException] { tx3.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } From 94ee58324dfeca98465fdc2471e807787a7cfe32 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 1 Jun 2026 14:49:32 +0200 Subject: [PATCH 4/5] Fix typo --- src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala index 625d841d..66347b19 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala @@ -366,7 +366,7 @@ object Transaction extends BtcSerializer[Transaction] { /** * Instead of using a pure Scala case class, we simply wrap an instance of bitcoin-kmp transaction. - * This is done because kmp<->scala conversion is expensive and to benefit from optmisations (such as + * This is done because kmp<->scala conversion is expensive and to benefit from optimisations (such as * pre-computed hashes used to sign transactions, see https://github.com/ACINQ/bitcoin-kmp/pull/184) * * But this also means that we cannot use `.copy()` to update inputs or outputs for example, and must provide From defabf93775f9ada8e1878b96ebcb21e83917475 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 2 Jun 2026 17:03:43 +0200 Subject: [PATCH 5/5] Revert Transaction to a pure scala case class with a lazy kmp conversion This will have less impact downstream and sill benefits from bitcoin-kmp optimisations. --- .../bitcoin/scalacompat/KotlinUtils.scala | 6 +- .../scalacompat/LexicographicalOrdering.scala | 2 +- .../fr/acinq/bitcoin/scalacompat/Script.scala | 2 +- .../bitcoin/scalacompat/Transaction.scala | 165 ++++++++---------- .../bitcoin/scalacompat/SighashSpec.scala | 4 +- 5 files changed, 80 insertions(+), 99 deletions(-) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala index 17acfaf2..725ffaab 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala @@ -84,11 +84,11 @@ object KotlinUtils { implicit def kmp2scala(input: bitcoin.TxOut): TxOut = TxOut(input.amount, input.publicKeyScript) - implicit def scala2kmp(input: TxOut): bitcoin.TxOut = input.kmp + implicit def scala2kmp(input: TxOut): bitcoin.TxOut = new bitcoin.TxOut(input.amount, input.publicKeyScript) - implicit def kmp2scala(input: bitcoin.Transaction): Transaction = Transaction(input) + implicit def kmp2scala(input: bitcoin.Transaction): Transaction = Transaction(input.version, input.txIn.asScala.toList.map(kmp2scala), input.txOut.asScala.toList.map(kmp2scala), input.lockTime) - implicit def scala2kmp(input: Transaction): bitcoin.Transaction = input.inner + implicit def scala2kmp(input: Transaction): bitcoin.Transaction = input.kmp implicit def kmp2scala(input: bitcoin.PrivateKey): PrivateKey = PrivateKey(input) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala index cbf981ca..86bda522 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/LexicographicalOrdering.scala @@ -27,5 +27,5 @@ object LexicographicalOrdering { * @param tx input transaction * @return the input tx with inputs and outputs sorted in lexicographical order */ - def sort(tx: Transaction): Transaction = Transaction(fr.acinq.bitcoin.LexicographicalOrdering.sort(tx.inner)) + def sort(tx: Transaction): Transaction = tx.copy(txIn = tx.txIn.sortWith(isLessThan), txOut = tx.txOut.sortWith(isLessThan)) } diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala index 215230f2..93d6b60d 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala @@ -52,7 +52,7 @@ object Script { * @param inputIndex 0-based index of the tx input that is being processed */ case class Context(tx: Transaction, inputIndex: Int, amount: Satoshi, prevouts: List[TxOut] = Nil) { - require(inputIndex >= 0 && inputIndex < tx.inner.txIn.size(), "invalid input index") + require(inputIndex >= 0 && inputIndex < tx.txIn.length, "invalid input index") } /** diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala index 66347b19..192edd2e 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala @@ -1,13 +1,13 @@ package fr.acinq.bitcoin.scalacompat -import fr.acinq.{bitcoin => bitcoinkmp} +import fr.acinq.bitcoin import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.bitcoin.scalacompat.Protocol._ import scodec.bits.ByteVector import java.io.{InputStream, OutputStream} -import scala.jdk.CollectionConverters.{ListHasAsScala, MapHasAsJava, SeqHasAsJava} +import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} /** * This is the double hash of a transaction serialized without witness data. @@ -42,9 +42,9 @@ object OutPoint extends BtcSerializer[OutPoint] { def apply(txid: TxId, index: Long): OutPoint = OutPoint(TxHash(txid), index) - override def read(input: InputStream, protocolVersion: Long): OutPoint = kmp2scala(bitcoinkmp.OutPoint.read(InputStreamWrapper(input), protocolVersion)) + override def read(input: InputStream, protocolVersion: Long): OutPoint = kmp2scala(fr.acinq.bitcoin.OutPoint.read(InputStreamWrapper(input), protocolVersion)) - override def write(input: OutPoint, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.OutPoint.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) + override def write(input: OutPoint, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.OutPoint.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) def isCoinbase(input: OutPoint): Boolean = scala2kmp(input).isCoinbase @@ -71,12 +71,12 @@ case class OutPoint(hash: TxHash, index: Long) extends BtcSerializable[OutPoint] object TxIn extends BtcSerializer[TxIn] { def apply(outPoint: OutPoint, signatureScript: Seq[ScriptElt], sequence: Long): TxIn = new TxIn(outPoint, Script.write(signatureScript), sequence) - override def read(input: InputStream, protocolVersion: Long): TxIn = kmp2scala(bitcoinkmp.TxIn.read(InputStreamWrapper(input), protocolVersion)) + override def read(input: InputStream, protocolVersion: Long): TxIn = kmp2scala(fr.acinq.bitcoin.TxIn.read(InputStreamWrapper(input), protocolVersion)) - override def write(input: TxIn, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.TxIn.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) + override def write(input: TxIn, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.TxIn.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) override def validate(input: TxIn): Unit = { - require(input.signatureScript.length <= bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE, s"signature script is ${input.signatureScript.length} bytes, limit is ${bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") + require(input.signatureScript.length <= bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE, s"signature script is ${input.signatureScript.length} bytes, limit is ${bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") } def coinbase(script: ByteVector): TxIn = { @@ -86,7 +86,7 @@ object TxIn extends BtcSerializer[TxIn] { def coinbase(script: Seq[ScriptElt]): TxIn = coinbase(Script.write(script)) - val SEQUENCE_FINAL: Long = bitcoinkmp.TxIn.SEQUENCE_FINAL + val SEQUENCE_FINAL: Long = fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL } /** @@ -99,7 +99,7 @@ object TxIn extends BtcSerializer[TxIn] { * @param witness Transaction witness (i.e. what is in sig script for standard transactions). */ case class TxIn(outPoint: OutPoint, signatureScript: ByteVector, sequence: Long, witness: ScriptWitness = ScriptWitness.empty) extends BtcSerializable[TxIn] { - def isFinal: Boolean = sequence == bitcoinkmp.TxIn.SEQUENCE_FINAL + def isFinal: Boolean = sequence == bitcoin.TxIn.SEQUENCE_FINAL def hasWitness: Boolean = witness.isNotNull def weight(): Int = scala2kmp(this).weight() @@ -109,15 +109,15 @@ case class TxIn(outPoint: OutPoint, signatureScript: ByteVector, sequence: Long, object TxOut extends BtcSerializer[TxOut] { def apply(amount: Satoshi, publicKeyScript: Seq[ScriptElt]): TxOut = new TxOut(amount, Script.write(publicKeyScript)) - override def read(input: InputStream, protocolVersion: Long): TxOut = kmp2scala(bitcoinkmp.TxOut.read(InputStreamWrapper(input), protocolVersion)) + override def read(input: InputStream, protocolVersion: Long): TxOut = kmp2scala(fr.acinq.bitcoin.TxOut.read(InputStreamWrapper(input), protocolVersion)) - override def write(input: TxOut, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.TxOut.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) + override def write(input: TxOut, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.TxOut.write(scala2kmp(input), OutputStreamWrapper(out), protocolVersion) override def validate(input: TxOut): Unit = { import input._ require(amount.toLong >= 0, s"invalid txout amount: $amount") require(amount.toLong <= BtcAmount.MaxMoney, s"invalid txout amount: $amount") - require(publicKeyScript.length < bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE, s"public key script is ${publicKeyScript.length} bytes, limit is ${bitcoinkmp.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") + require(publicKeyScript.length < bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE, s"public key script is ${publicKeyScript.length} bytes, limit is ${bitcoin.Script.MAX_SCRIPT_ELEMENT_SIZE} bytes") } } @@ -128,10 +128,7 @@ object TxOut extends BtcSerializer[TxOut] { * @param publicKeyScript public key script which sets the conditions for spending this output */ case class TxOut(amount: Satoshi, publicKeyScript: ByteVector) extends BtcSerializable[TxOut] { - // TxOut will almost always need to be converted to its bitcoin-kmp counterpart so we precompute this conversion here - val kmp: bitcoinkmp.TxOut = new bitcoinkmp.TxOut(amount, publicKeyScript.toArrayUnsafe) - - def weight(): Int = kmp.weight() + def weight(): Int = scala2kmp(this).weight() override def serializer: BtcSerializer[TxOut] = TxOut } @@ -139,9 +136,9 @@ case class TxOut(amount: Satoshi, publicKeyScript: ByteVector) extends BtcSerial object ScriptWitness extends BtcSerializer[ScriptWitness] { val empty: ScriptWitness = ScriptWitness(Seq.empty[ByteVector]) - override def write(t: ScriptWitness, out: OutputStream, protocolVersion: Long): Unit = bitcoinkmp.ScriptWitness.write(scala2kmp(t), OutputStreamWrapper(out), protocolVersion) + override def write(t: ScriptWitness, out: OutputStream, protocolVersion: Long): Unit = fr.acinq.bitcoin.ScriptWitness.write(scala2kmp(t), OutputStreamWrapper(out), protocolVersion) - override def read(in: InputStream, protocolVersion: Long): ScriptWitness = kmp2scala(bitcoinkmp.ScriptWitness.read(InputStreamWrapper(in), protocolVersion)) + override def read(in: InputStream, protocolVersion: Long): ScriptWitness = kmp2scala(fr.acinq.bitcoin.ScriptWitness.read(InputStreamWrapper(in), protocolVersion)) } /** @@ -159,45 +156,33 @@ case class ScriptWitness(stack: Seq[ByteVector]) extends BtcSerializable[ScriptW } object Transaction extends BtcSerializer[Transaction] { - /** - * - * @param version Transaction data format version - * @param txIn Transaction inputs - * @param txOut Transaction outputs - * @param lockTime The block number or timestamp at which this transaction is locked - * @return a new transaction - */ - def apply(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTime: Long): Transaction = Transaction( - new bitcoinkmp.Transaction(version, txIn.map(scala2kmp).asJava, txOut.map(scala2kmp).asJava, lockTime) - ) - /** * * @param version protocol version (and NOT transaction version !) * @return true if protocol version specifies that witness data is to be serialized */ - def serializeTxWitness(version: Long): Boolean = bitcoinkmp.Transaction.serializeTxWitness(version) + def serializeTxWitness(version: Long): Boolean = fr.acinq.bitcoin.Transaction.serializeTxWitness(version) override def read(input: InputStream, protocolVersion: Long): Transaction = { - val tx = bitcoinkmp.Transaction.read(InputStreamWrapper(input), protocolVersion) - Transaction(tx) + val tx = fr.acinq.bitcoin.Transaction.read(InputStreamWrapper(input), protocolVersion) + tx } override def write(tx: Transaction, out: OutputStream, protocolVersion: Long): Unit = { - bitcoinkmp.Transaction.write(tx.inner, OutputStreamWrapper(out), protocolVersion) + fr.acinq.bitcoin.Transaction.write(tx, OutputStreamWrapper(out), protocolVersion) } override def validate(input: Transaction): Unit = { - bitcoinkmp.Transaction.validate(input.inner) + fr.acinq.bitcoin.Transaction.validate(input) } - def baseSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = tx.inner.baseSize(protocolVersion) + def baseSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = fr.acinq.bitcoin.Transaction.baseSize(scala2kmp(tx), protocolVersion) - def totalSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = tx.inner.baseSize(protocolVersion) + def totalSize(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = fr.acinq.bitcoin.Transaction.totalSize(scala2kmp(tx), protocolVersion) - def weight(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = tx.inner.weight(protocolVersion) + def weight(tx: Transaction, protocolVersion: Long = PROTOCOL_VERSION): Int = totalSize(tx, protocolVersion) + 3 * baseSize(tx, protocolVersion) - def isCoinbase(input: Transaction): Boolean = input.inner.isCoinbase + def isCoinbase(input: Transaction): Boolean = input.txIn.size == 1 && OutPoint.isCoinbase(input.txIn.head.outPoint) /** * prepare a transaction for signing a specific input @@ -222,7 +207,7 @@ object Transaction extends BtcSerializer[Transaction] { * @return a hash which can be used to sign the referenced tx input */ def hashForSigning(tx: Transaction, inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int): ByteVector32 = { - ByteVector32(ByteVector.view(bitcoinkmp.Transaction.hashForSigning(tx, inputIndex, previousOutputScript.toArrayUnsafe, sighashType))) + ByteVector32(ByteVector.view(fr.acinq.bitcoin.Transaction.hashForSigning(tx, inputIndex, previousOutputScript.toArrayUnsafe, sighashType))) } /** @@ -323,7 +308,7 @@ object Transaction extends BtcSerializer[Transaction] { * @param sighashType signature hash type, which will be appended to the signature * @return an ECDSA signature in the format used in transaction witnesses: DER encoded followed by a sighash byte */ - def encodeWitnessEcdsaSig(sig: ByteVector64, sighashType: Int): ByteVector = ByteVector.view(bitcoinkmp.Transaction.encodeWitnessEcdsaSig(sig, sighashType)) + def encodeWitnessEcdsaSig(sig: ByteVector64, sighashType: Int): ByteVector = ByteVector.view(fr.acinq.bitcoin.Transaction.encodeWitnessEcdsaSig(sig, sighashType)) /** * Sign a taproot tx input, using the internal key path. @@ -365,92 +350,88 @@ object Transaction extends BtcSerializer[Transaction] { } /** - * Instead of using a pure Scala case class, we simply wrap an instance of bitcoin-kmp transaction. - * This is done because kmp<->scala conversion is expensive and to benefit from optimisations (such as - * pre-computed hashes used to sign transactions, see https://github.com/ACINQ/bitcoin-kmp/pull/184) - * - * But this also means that we cannot use `.copy()` to update inputs or outputs for example, and must provide - * helper methods instead. + * Transaction * + * @param version Transaction data format version + * @param txIn Transaction inputs + * @param txOut Transaction outputs + * @param lockTime The block number or timestamp at which this transaction is locked */ -case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Transaction] { - val version: Long = inner.version - val lockTime: Long = inner.lockTime +case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTime: Long) extends BtcSerializable[Transaction] { + lazy val kmp: fr.acinq.bitcoin.Transaction = new fr.acinq.bitcoin.Transaction(version, txIn.map(scala2kmp).asJava, txOut.map(scala2kmp).asJava, lockTime) // standard transaction hash, used to identify transactions (in transactions outputs for example) - lazy val hash: TxHash = kmp2scala(inner.hash) - lazy val txid: TxId = kmp2scala(inner.txid) - + lazy val hash: TxHash = kmp.hash + lazy val txid: TxId = TxId(hash) // witness transaction hash that includes witness data. used to compute the witness commitment included in the coinbase // transaction of segwit blocks - lazy val whash: ByteVector32 = Crypto.hash256(bin) + lazy val whash: ByteVector32 = Crypto.hash256(Transaction.write(this)) lazy val wtxid: ByteVector32 = whash.reverse lazy val bin: ByteVector = Transaction.write(this) // this is much easier to use than Scala's default toString override def toString: String = bin.toHex - lazy val txOut: Seq[TxOut] = inner.txOut.asScala.map(kmp2scala).toSeq - - lazy val txIn: Seq[TxIn] = inner.txIn.asScala.map(kmp2scala).toSeq - - def updateInputs(txIn: Seq[TxIn]) : Transaction = Transaction(inner.updateInputs(txIn.map(scala2kmp).asJava)) - - def updateOutputs(txOut: Seq[TxOut]) : Transaction = Transaction(inner.updateOutputs(txOut.map(scala2kmp).asJava)) - - def updateInputsAndOutputs(txIn: Seq[TxIn], txOut: Seq[TxOut]) : Transaction = Transaction(inner.copy(inner.version, txIn.map(scala2kmp).asJava, txOut.map(scala2kmp).asJava, inner.lockTime)) - - def updateLockTime(lockTime: Long): Transaction = Transaction(inner.copy(inner.version, inner.txIn, inner.txOut, lockTime)) - /** * * @param blockHeight current block height * @param blockTime current block time * @return true if the transaction is final */ - def isFinal(blockHeight: Long, blockTime: Long): Boolean = inner.isFinal(blockHeight, blockTime) + def isFinal(blockHeight: Long, blockTime: Long): Boolean = lockTime match { + case 0 => true + case value if value < bitcoin.Transaction.LOCKTIME_THRESHOLD && value < blockHeight => true + case value if value >= bitcoin.Transaction.LOCKTIME_THRESHOLD && value < blockTime => true + case _ if txIn.exists(!_.isFinal) => false + case _ => true + } /** * * @param i index of the tx input to update * @param sigScript new signature script - * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replaced by sigScript + * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replace by sigscript */ - def updateSigScript(i: Int, sigScript: ByteVector): Transaction = Transaction(inner.updateSigScript(i, sigScript.toArrayUnsafe)) + def updateSigScript(i: Int, sigScript: ByteVector): Transaction = this.copy(txIn = txIn.updated(i, txIn(i).copy(signatureScript = sigScript))) /** * * @param i index of the tx input to update * @param sigScript new signature script - * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replaced by sigScript + * @return a new transaction that is of copy of this one but where the signature script of the ith input has been replace by sigscript */ - def updateSigScript(i: Int, sigScript: Seq[ScriptElt]): Transaction = Transaction(inner.updateSigScript(i, sigScript.map(scala2kmp).asJava)) + def updateSigScript(i: Int, sigScript: Seq[ScriptElt]): Transaction = updateSigScript(i, Script.write(sigScript)) - def updateWitness(i: Int, witness: ScriptWitness): Transaction = Transaction(inner.updateWitness(i, scala2kmp(witness))) + def updateWitness(i: Int, witness: ScriptWitness): Transaction = this.copy(txIn = txIn.updated(i, txIn(i).copy(witness = witness))) - def updateWitnesses(witnesses: Seq[ScriptWitness]): Transaction = Transaction(inner.updateWitnesses(witnesses.map(scala2kmp).asJava)) + def updateWitnesses(witnesses: Seq[ScriptWitness]): Transaction = { + require(witnesses.length == txIn.length) + witnesses.zipWithIndex.foldLeft(this) { + case (tx, (witness, index)) => tx.updateWitness(index, witness) + } + } - def hasWitness: Boolean = inner.getHasWitness + def hasWitness: Boolean = txIn.exists(_.hasWitness) /** * * @param input input to add the tx * @return a new transaction which includes the newly added input */ - def addInput(input: TxIn): Transaction = this.copy(inner = inner.addInput(scala2kmp(input))) + def addInput(input: TxIn): Transaction = this.copy(txIn = this.txIn :+ input) /** * * @param output output to add to the tx * @return a new transaction which includes the newly added output */ - def addOutput(output: TxOut): Transaction = this.copy(inner = inner.addOutput(scala2kmp(output))) + def addOutput(output: TxOut): Transaction = this.copy(txOut = this.txOut :+ output) - def baseSize(protocolVersion: Long = PROTOCOL_VERSION): Int = inner.baseSize(protocolVersion) + def baseSize(protocolVersion: Long = PROTOCOL_VERSION): Int = kmp.baseSize(protocolVersion) - def totalSize(protocolVersion: Long = PROTOCOL_VERSION): Int = inner.totalSize(protocolVersion) + def totalSize(protocolVersion: Long = PROTOCOL_VERSION): Int = kmp.totalSize(protocolVersion) - def weight(protocolVersion: Long = PROTOCOL_VERSION): Int = inner.weight(protocolVersion) + def weight(protocolVersion: Long = PROTOCOL_VERSION): Int = kmp.weight(protocolVersion) /** * prepare a transaction for signing a specific input @@ -461,7 +442,7 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return a new transaction with proper inputs and outputs according to SIGHASH_TYPE rules */ def prepareForSigning(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int): Transaction = { - Transaction(inner.prepareForSigning(inputIndex, previousOutputScript.toArrayUnsafe, sighashType)) + kmp.prepareForSigning(inputIndex, previousOutputScript.toArray, sighashType) } /** @@ -474,7 +455,7 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return a hash which can be used to sign the referenced tx input */ def hashForSigning(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int, amount: Satoshi, signatureVersion: Int): ByteVector32 = { - ByteVector32(ByteVector.view(inner.hashForSigning(inputIndex, previousOutputScript.toArrayUnsafe, sighashType, amount, signatureVersion))) + ByteVector32(ByteVector.view(kmp.hashForSigning(inputIndex, previousOutputScript.toArrayUnsafe, sighashType, amount, signatureVersion))) } /** @@ -498,17 +479,17 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @param annex_opt (optional) taproot annex */ def hashForSigningSchnorr(inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, sigVersion: Int, tapleaf_opt: Option[ByteVector32] = None, annex_opt: Option[ByteVector] = None): ByteVector32 = { - inner.hashForSigningSchnorr(inputIndex, inputs.map(scala2kmp).asJava, sighashType, sigVersion, tapleaf_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, null) + kmp.hashForSigningSchnorr(inputIndex, inputs.map(scala2kmp).asJava, sighashType, sigVersion, tapleaf_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, null) } /** Use this function when spending a taproot key path. */ def hashForSigningTaprootKeyPath(inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, annex_opt: Option[ByteVector] = None): ByteVector32 = { - inner.hashForSigningTaprootKeyPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, annex_opt.map(scala2kmp).orNull) + kmp.hashForSigningTaprootKeyPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, annex_opt.map(scala2kmp).orNull) } /** Use this function when spending a taproot script path. */ def hashForSigningTaprootScriptPath(inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None): ByteVector32 = { - inner.hashForSigningTaprootScriptPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, scala2kmp(tapleaf), annex_opt.map(scala2kmp).orNull) + kmp.hashForSigningTaprootScriptPath(inputIndex, inputs.map(scala2kmp).asJava, sighashType, scala2kmp(tapleaf), annex_opt.map(scala2kmp).orNull) } /** @@ -523,7 +504,7 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return the encoded signature of this tx for this specific tx input in compact 64 bytes format */ def signInputCompact(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector64 = { - inner.signInputCompact(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv) + kmp.signInputCompact(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv) } /** @@ -538,7 +519,7 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return the encoded signature of this tx for this specific tx input in compact 64 bytes format */ def signInputCompact(inputIndex: Int, previousOutputScript: Seq[ScriptElt], sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector64 = - inner.signInputCompact(inputIndex, previousOutputScript.map(scala2kmp).asJava, sighashType, amount, signatureVersion, privateKey.priv) + signInputCompact(inputIndex, Script.write(previousOutputScript), sighashType, amount, signatureVersion, privateKey) /** @@ -553,13 +534,13 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return the encoded signature of this tx for this specific tx input */ def signInput(inputIndex: Int, previousOutputScript: ByteVector, sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector = { - ByteVector.view(inner.signInput(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv)) + ByteVector.view(kmp.signInput(inputIndex, scala2kmp(previousOutputScript), sighashType, amount, signatureVersion, privateKey.priv)) } /** * sign a tx input * - * @param inputIndex index of the input that is being processed + * @param inputIndex index of the tx input that is being processed * @param previousOutputScript public key script of the output claimed by this tx input * @param sighashType signature hash type, which will be appended to the signature * @param amount amount of the output claimed by this tx input @@ -581,7 +562,7 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return the schnorr signature of this tx for this specific tx input. */ def signInputTaprootKeyPath(privateKey: PrivateKey, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, scriptTree_opt: Option[ScriptTree], annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = { - inner.signInputTaprootKeyPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) + kmp.signInputTaprootKeyPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.map(scala2kmp).orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) } /** @@ -595,15 +576,15 @@ case class Transaction(inner: bitcoinkmp.Transaction) extends BtcSerializable[Tr * @return the schnorr signature of this tx for this specific tx input and the given script leaf. */ def signInputTaprootScriptPath(privateKey: PrivateKey, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = { - inner.signInputTaprootScriptPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, tapleaf, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) + kmp.signInputTaprootScriptPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, tapleaf, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull) } def correctlySpends(previousOutputs: Map[OutPoint, TxOut], scriptFlags: Int): Unit = { - inner.correctlySpends(previousOutputs.map { case (o, t) => scala2kmp(o) -> scala2kmp(t) }.asJava, scriptFlags) + kmp.correctlySpends(previousOutputs.map { case (o, t) => scala2kmp(o) -> scala2kmp(t) }.asJava, scriptFlags) } def correctlySpends(inputs: Seq[Transaction], scriptFlags: Int): Unit = { - inner.correctlySpends(inputs.map(scala2kmp).asJava, scriptFlags) + kmp.correctlySpends(inputs.map(scala2kmp).asJava, scriptFlags) } override def serializer: BtcSerializer[Transaction] = Transaction diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala index c7ad06e5..774b9e48 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/SighashSpec.scala @@ -45,7 +45,7 @@ class SighashSpec extends FunSuite { tx2.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // but I cannot change the tx output - val tx3 = Transaction(tx2.version, tx2.txIn, tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc)), tx2.lockTime) + val tx3 = tx2.copy(txOut = tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc))) intercept[RuntimeException] { tx3.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -84,7 +84,7 @@ class SighashSpec extends FunSuite { tx2.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // but I cannot change the tx output - val tx3 = Transaction(tx2.version, tx2.txIn, tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc)), tx2.lockTime) + val tx3 = tx2.copy(txOut = tx2.txOut.updated(0, tx2.txOut.head.copy(amount = 40 millibtc))) intercept[RuntimeException] { tx3.correctlySpends(previousTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }