diff --git a/pom.xml b/pom.xml
index c564eb6d..b742cc41 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
fr.acinq
bitcoin-lib_2.13
jar
- 0.43
+ 0.43.1
Simple Scala Bitcoin library
https://github.com/ACINQ/bitcoin-lib
bitcoin-lib
diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
index 660b8a57..35db3acb 100644
--- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
+++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
@@ -5,6 +5,27 @@ import fr.acinq.bitcoin.scalacompat.KotlinUtils._
import scodec.bits.ByteVector
object Crypto {
+
+ // @formatter:off
+ /** Specify how private keys are tweaked when creating Schnorr signatures. */
+ sealed trait SchnorrTweak
+ object SchnorrTweak {
+ /** The private key is directly used, without any tweaks. */
+ case object NoTweak extends SchnorrTweak
+ }
+
+ sealed trait TaprootTweak extends SchnorrTweak
+ object TaprootTweak {
+ /** The private key is tweaked with H_TapTweak(public key) (this is used for key path spending when there is no script tree). */
+ case object NoScriptTweak extends TaprootTweak
+ /** The private key is tweaked with H_TapTweak(public key || merkle_root) (this is used for key path spending when a script tree exists). */
+ case class ScriptTweak(merkleRoot: ByteVector32) extends TaprootTweak
+ object ScriptTweak {
+ def apply(scriptTree: ScriptTree): ScriptTweak = ScriptTweak(scriptTree.hash())
+ }
+ }
+ // @formatter:on
+
/**
* A bitcoin private key.
* A private key is valid if it is not 0 and less than the secp256k1 curve order when interpreted as an integer (most significant byte first).
@@ -124,7 +145,7 @@ object Crypto {
case class XonlyPublicKey(pub: bitcoin.XonlyPublicKey) {
val publicKey: PublicKey = PublicKey(pub.getPublicKey)
- def tweak(tapTweak: bitcoin.Crypto.TaprootTweak): ByteVector32 = pub.tweak(tapTweak)
+ def tweak(tapTweak: TaprootTweak): ByteVector32 = pub.tweak(scala2kmp(tapTweak))
/**
* "tweaks" this key with an optional merkle root
@@ -132,16 +153,16 @@ object Crypto {
* @param tapTweak taproot tweak
* @return an (x-only pubkey, parity) pair
*/
- def outputKey(tapTweak: bitcoin.Crypto.TaprootTweak): (XonlyPublicKey, Boolean) = {
- val p = pub.outputKey(tapTweak)
+ def outputKey(tapTweak: TaprootTweak): (XonlyPublicKey, Boolean) = {
+ val p = pub.outputKey(scala2kmp(tapTweak))
(XonlyPublicKey(p.getFirst), p.getSecond)
}
/** Tweak this key with the merkle root of the given script tree. */
- def outputKey(scriptTree: bitcoin.ScriptTree): (XonlyPublicKey, Boolean) = outputKey(new bitcoin.Crypto.TaprootTweak.ScriptTweak(scriptTree))
+ def outputKey(scriptTree: ScriptTree): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptTweak(scriptTree))
/** Tweak this key with the merkle root provided. */
- def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(new bitcoin.Crypto.TaprootTweak.ScriptTweak(merkleRoot))
+ def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptTweak(merkleRoot))
/**
* add a public key to this x-only key
@@ -274,8 +295,8 @@ object Crypto {
* the key (there is an extra "1" appended to the key)
* @return a signature in compact format (64 bytes)
*/
- def signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak: bitcoin.Crypto.SchnorrTweak = bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
- bitcoin.Crypto.signSchnorr(data, privateKey, schnorrTweak, auxrand32.map(scala2kmp).orNull)
+ def signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak: SchnorrTweak = SchnorrTweak.NoTweak, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
+ bitcoin.Crypto.signSchnorr(data, privateKey, scala2kmp(schnorrTweak), auxrand32.map(scala2kmp).orNull)
}
/**
diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala
index 9917cb9a..880ccc67 100644
--- a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala
+++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala
@@ -1,13 +1,14 @@
package fr.acinq.bitcoin.scalacompat
import fr.acinq.bitcoin
-import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
+import fr.acinq.bitcoin.scalacompat.Crypto._
import scodec.bits.ByteVector
import java.io.{InputStream, OutputStream}
import scala.jdk.CollectionConverters.{ListHasAsScala, SeqHasAsJava}
object KotlinUtils {
+
implicit def kmp2scala(input: bitcoin.ByteVector32): ByteVector32 = ByteVector32(ByteVector(input.toByteArray))
implicit def scala2kmp(input: ByteVector32): bitcoin.ByteVector32 = new bitcoin.ByteVector32(input.toArray)
@@ -44,6 +45,47 @@ object KotlinUtils {
implicit def scala2kmp(input: ScriptWitness): bitcoin.ScriptWitness = new bitcoin.ScriptWitness(input.stack.map(scala2kmp).asJava)
+ implicit def kmp2scala(input: bitcoin.ScriptTree.Leaf): ScriptTree.Leaf = ScriptTree.Leaf(kmp2scala(input.getScript), input.getLeafVersion)
+
+ implicit def scala2kmp(input: ScriptTree.Leaf): bitcoin.ScriptTree.Leaf = new bitcoin.ScriptTree.Leaf(scala2kmp(input.script), input.leafVersion)
+
+ implicit def kmp2scala(input: bitcoin.ScriptTree.Branch): ScriptTree.Branch = ScriptTree.Branch(kmp2scala(input.getLeft), kmp2scala(input.getRight))
+
+ implicit def scala2kmp(input: ScriptTree.Branch): bitcoin.ScriptTree.Branch = new bitcoin.ScriptTree.Branch(scala2kmp(input.left), scala2kmp(input.right))
+
+ implicit def kmp2scala(input: bitcoin.ScriptTree): ScriptTree = input match {
+ case branch: bitcoin.ScriptTree.Branch => kmp2scala(branch)
+ case leaf: bitcoin.ScriptTree.Leaf => kmp2scala(leaf)
+ case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
+ }
+
+ implicit def scala2kmp(input: ScriptTree): bitcoin.ScriptTree = input match {
+ case leaf: ScriptTree.Leaf => scala2kmp(leaf)
+ case branch: ScriptTree.Branch => scala2kmp(branch)
+ }
+
+ implicit def kmp2scala(input: bitcoin.Crypto.TaprootTweak): TaprootTweak = input match {
+ case bitcoin.Crypto.TaprootTweak.NoScriptTweak.INSTANCE => TaprootTweak.NoScriptTweak
+ case tweak: bitcoin.Crypto.TaprootTweak.ScriptTweak => TaprootTweak.ScriptTweak(kmp2scala(tweak.getMerkleRoot))
+ case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
+ }
+
+ implicit def scala2kmp(input: TaprootTweak): bitcoin.Crypto.TaprootTweak = input match {
+ case TaprootTweak.NoScriptTweak => bitcoin.Crypto.TaprootTweak.NoScriptTweak.INSTANCE
+ case tweak: TaprootTweak.ScriptTweak => new bitcoin.Crypto.TaprootTweak.ScriptTweak(scala2kmp(tweak.merkleRoot))
+ }
+
+ implicit def kmp2scala(input: bitcoin.Crypto.SchnorrTweak): SchnorrTweak = input match {
+ case bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE => SchnorrTweak.NoTweak
+ case tweak: bitcoin.Crypto.TaprootTweak => kmp2scala(tweak)
+ case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
+ }
+
+ implicit def scala2kmp(input: SchnorrTweak): bitcoin.Crypto.SchnorrTweak = input match {
+ case SchnorrTweak.NoTweak => bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE
+ case tweak: TaprootTweak => scala2kmp(tweak)
+ }
+
implicit def kmp2scala(input: bitcoin.TxIn): TxIn = TxIn(input.outPoint, input.signatureScript, input.sequence, input.witness)
implicit def scala2kmp(input: Satoshi): bitcoin.Satoshi = new bitcoin.Satoshi(input.toLong)
@@ -229,5 +271,6 @@ object KotlinUtils {
OP_INVALIDOPCODE -> bitcoin.OP_INVALIDOPCODE.INSTANCE)
private val scriptEltMapKmp2Scala2Map: Map[bitcoin.ScriptElt, ScriptElt] = scriptEltMapScala2Kmp.map { case (k, v) => v -> k }
+
}
diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
index 3d7e4823..2c601435 100644
--- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
+++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
@@ -1,14 +1,32 @@
package fr.acinq.bitcoin.scalacompat
-import fr.acinq.bitcoin.ScriptTree
-import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
+import fr.acinq.bitcoin.crypto.musig2
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
+import fr.acinq.secp256k1.Secp256k1
+import scodec.bits.ByteVector
import scala.jdk.CollectionConverters.SeqHasAsJava
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)
+
+ /**
+ * Musig2 public nonce, that must be shared with other participants in the signing session.
+ * It contains two elliptic curve points, but should be treated as an opaque blob.
+ */
+ case class IndividualNonce(data: ByteVector) {
+ require(data.size == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE, "invalid musig2 public nonce size")
+ }
+
+ /** A locally-generated nonce, for which both the secret and public parts are known. */
+ case class LocalNonce(secretNonce: SecretNonce, publicNonce: IndividualNonce)
+
/**
* Aggregate the public keys of a musig2 session into a single public key.
* Note that this function doesn't apply any tweak: when used for taproot, it computes the internal public key, not
@@ -16,18 +34,18 @@ object Musig2 {
*
* @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
*/
- def aggregateKeys(publicKeys: Seq[PublicKey]): XonlyPublicKey = XonlyPublicKey(fr.acinq.bitcoin.crypto.musig2.Musig2.aggregateKeys(publicKeys.map(scala2kmp).asJava))
+ def aggregateKeys(publicKeys: Seq[PublicKey]): XonlyPublicKey = XonlyPublicKey(musig2.Musig2.aggregateKeys(publicKeys.map(scala2kmp).asJava))
/**
- * @param sessionId a random, unique session ID.
- * @param signingKey either the signer's private key or public key
- * @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
- * @param message_opt (optional) message that will be signed, if already known.
+ * @param sessionId a random, unique session ID.
+ * @param signingKey either the signer's private key or public key
+ * @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
+ * @param message_opt (optional) message that will be signed, if already known.
* @param extraInput_opt (optional) additional random data.
*/
- def generateNonce(sessionId: ByteVector32, signingKey: Either[PrivateKey, PublicKey], publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): (SecretNonce, IndividualNonce) = {
- val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, either2keitherkmp(signingKey.map(scala2kmp).left.map(scala2kmp)), publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
- (nonce.getFirst, nonce.getSecond)
+ def generateNonce(sessionId: ByteVector32, signingKey: Either[PrivateKey, PublicKey], publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): LocalNonce = {
+ val nonce = musig2.Musig2.generateNonce(sessionId, either2keitherkmp(signingKey.map(scala2kmp).left.map(scala2kmp)), publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
+ LocalNonce(SecretNonce(nonce.getFirst), IndividualNonce(nonce.getSecond.getData))
}
/**
@@ -37,9 +55,9 @@ object Musig2 {
* @param message_opt (optional) message that will be signed, if already known.
* @param extraInput_opt (optional) additional random data.
*/
- def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): (SecretNonce, IndividualNonce) = {
- val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
- (nonce.getFirst, nonce.getSecond)
+ def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): LocalNonce = {
+ val nonce = musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
+ LocalNonce(SecretNonce(nonce.getFirst), IndividualNonce(nonce.getSecond.getData))
}
/**
@@ -55,7 +73,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] = {
- fr.acinq.bitcoin.crypto.musig2.Musig2.signTaprootInput(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, secretNonce, publicNonces.asJava, scriptTree_opt.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.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala)
}
/**
@@ -73,7 +91,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 = {
- fr.acinq.bitcoin.crypto.musig2.Musig2.verify(partialSig, nonce, publicKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.asJava, scriptTree_opt.orNull)
+ 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)
}
/**
@@ -88,7 +106,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] = {
- fr.acinq.bitcoin.crypto.musig2.Musig2.aggregateTaprootSignatures(partialSigs.map(scala2kmp).asJava, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.asJava, scriptTree_opt.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.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala)
}
}
diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
index ab914ef0..808b5fd4 100644
--- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
+++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
@@ -175,7 +175,7 @@ object Script {
* @param internalKey internal public key that will be tweaked with the [scripts] provided.
* @param scripts_opt optional spending scripts that can be used instead of key-path spending.
*/
- def pay2tr(internalKey: XonlyPublicKey, scripts_opt: Option[bitcoin.ScriptTree]): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, scripts_opt.orNull).asScala.map(kmp2scala).toList
+ def pay2tr(internalKey: XonlyPublicKey, scripts_opt: Option[ScriptTree]): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, scripts_opt.map(scala2kmp).orNull).asScala.map(kmp2scala).toList
def isPay2tr(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPay2tr(script.map(scala2kmp).asJava)
@@ -188,6 +188,6 @@ object Script {
* @param witness witness for the spent [script].
* @param scriptTree tapscript tree.
*/
- def witnessScriptPathPay2tr(internalKey: XonlyPublicKey, script: bitcoin.ScriptTree.Leaf, witness: ScriptWitness, scriptTree: bitcoin.ScriptTree): ScriptWitness = bitcoin.Script.witnessScriptPathPay2tr(internalKey.pub, script, witness, scriptTree)
+ def witnessScriptPathPay2tr(internalKey: XonlyPublicKey, script: ScriptTree.Leaf, witness: ScriptWitness, scriptTree: ScriptTree): ScriptWitness = bitcoin.Script.witnessScriptPathPay2tr(internalKey.pub, scala2kmp(script), witness, scala2kmp(scriptTree))
}
diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/ScriptTree.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/ScriptTree.scala
new file mode 100644
index 00000000..ebc6e7d8
--- /dev/null
+++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/ScriptTree.scala
@@ -0,0 +1,56 @@
+package fr.acinq.bitcoin.scalacompat
+
+import scodec.bits.ByteVector
+
+/** Simple binary tree structure containing taproot spending scripts. */
+sealed trait ScriptTree {
+
+ /** Compute the merkle root of the script tree. */
+ def hash(): ByteVector32 = KotlinUtils.kmp2scala(KotlinUtils.scala2kmp(this).hash())
+
+ /** Return the first leaf with a matching script, if any. */
+ def findScript(script: ByteVector): Option[ScriptTree.Leaf] = this match {
+ case leaf: ScriptTree.Leaf if leaf.script == script => Some(leaf)
+ case _: ScriptTree.Leaf => None
+ case branch: ScriptTree.Branch => branch.left.findScript(script).orElse(branch.right.findScript(script))
+ }
+
+ /** Return the first leaf with a matching leaf hash, if any. */
+ def findScript(leafHash: ByteVector32): Option[ScriptTree.Leaf] = this match {
+ case leaf: ScriptTree.Leaf if leaf.hash() == leafHash => Some(leaf)
+ case _: ScriptTree.Leaf => None
+ case branch: ScriptTree.Branch => branch.left.findScript(leafHash).orElse(branch.right.findScript(leafHash))
+ }
+
+ /**
+ * Compute a merkle proof for the given script leaf.
+ * This merkle proof is encoded for creating control blocks in taproot script path witnesses.
+ * If the leaf doesn't belong to the script tree, this function will return None.
+ */
+ def merkleProof(leafHash: ByteVector32): Option[ByteVector] = {
+ val proof_opt = KotlinUtils.scala2kmp(this).merkleProof(KotlinUtils.scala2kmp(leafHash))
+ if (proof_opt == null) None else Some(ByteVector(proof_opt))
+ }
+
+}
+
+object ScriptTree {
+ /**
+ * Multiple spending scripts can be placed in the leaves of a taproot tree. When using one of those scripts to spend
+ * funds, we only need to reveal that specific script and a merkle proof that it is a leaf of the tree.
+ *
+ * @param script serialized spending script.
+ * @param leafVersion tapscript version.
+ */
+ case class Leaf(script: ByteVector, leafVersion: Int) extends ScriptTree
+
+ object Leaf {
+ // @formatter:off
+ def apply(script: ByteVector): Leaf = Leaf(script, fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT)
+ def apply(script: Seq[ScriptElt]): Leaf = Leaf(script, fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT)
+ def apply(script: Seq[ScriptElt], leafVersion: Int): Leaf = Leaf(Script.write(script), leafVersion)
+ // @formatter:on
+ }
+
+ case class Branch(left: ScriptTree, right: ScriptTree) extends ScriptTree
+}
diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala
index 5490ab8a..e01ad5ca 100644
--- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala
+++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala
@@ -100,8 +100,8 @@ object TxIn extends BtcSerializer[TxIn] {
*/
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 hasWitness: Boolean = witness.isNotNull
+ def weight(): Int = scala2kmp(this).weight()
override def serializer: BtcSerializer[TxIn] = TxIn
}
@@ -128,6 +128,8 @@ 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()
+
override def serializer: BtcSerializer[TxOut] = TxOut
}
@@ -312,7 +314,7 @@ object Transaction extends BtcSerializer[Transaction] {
* @param scriptTree_opt tapscript tree of the signed input, if it has script paths.
* @return the schnorr signature of this tx for this specific tx input.
*/
- def signInputTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, scriptTree_opt: Option[bitcoin.ScriptTree], annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
+ def signInputTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, scriptTree_opt: Option[ScriptTree], annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
tx.signInputTaprootKeyPath(privateKey, inputIndex, inputs, sighashType, scriptTree_opt, annex_opt, auxrand32)
}
@@ -521,8 +523,8 @@ case class Transaction(version: Long, txIn: Seq[TxIn], txOut: Seq[TxOut], lockTi
* @param scriptTree_opt tapscript tree of the signed input, if it has script paths.
* @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[bitcoin.ScriptTree], annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
- scala2kmp(this).signInputTaprootKeyPath(privateKey, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull)
+ 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)
}
/**
diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
index 9311bd19..55f24e1f 100644
--- a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
+++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
@@ -1,12 +1,10 @@
package fr.acinq.bitcoin.scalacompat
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
-import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash}
-import fr.acinq.secp256k1.Hex
+import fr.acinq.bitcoin.{ScriptFlags, SigHash}
import org.scalatest.FunSuite
import scodec.bits.{ByteVector, HexStringSyntax}
-import scala.jdk.CollectionConverters.SeqHasAsJava
import scala.util.Random
class Musig2Spec extends FunSuite {
@@ -27,15 +25,15 @@ class Musig2Spec extends FunSuite {
// The first step of a musig2 signing session is to exchange nonces.
// If participants are disconnected before the end of the signing session, they must start again with fresh nonces.
- val (aliceSecretNonce, alicePublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(alicePrivKey), Seq(alicePubKey, bobPubKey), None, None)
- val (bobSecretNonce, bobPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Right(bobPrivKey.publicKey), Seq(alicePubKey, bobPubKey), None, None)
+ val aliceNonce = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(alicePrivKey), Seq(alicePubKey, bobPubKey), None, None)
+ val bobNonce = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Right(bobPrivKey.publicKey), Seq(alicePubKey, bobPubKey), None, None)
// Once they have each other's public nonce, they can produce partial signatures.
- val publicNonces = Seq(alicePublicNonce, bobPublicNonce)
- val Right(aliceSig) = Musig2.signTaprootInput(alicePrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), aliceSecretNonce, publicNonces, scriptTree_opt = None)
- assert(Musig2.verifyTaprootSignature(aliceSig, alicePublicNonce, alicePubKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None))
- val Right(bobSig) = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), bobSecretNonce, publicNonces, scriptTree_opt = None)
- assert(Musig2.verifyTaprootSignature(bobSig, bobPublicNonce, bobPubKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None))
+ val publicNonces = Seq(aliceNonce, bobNonce).map(_.publicNonce)
+ val Right(aliceSig) = Musig2.signTaprootInput(alicePrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), aliceNonce.secretNonce, publicNonces, scriptTree_opt = None)
+ assert(Musig2.verifyTaprootSignature(aliceSig, aliceNonce.publicNonce, alicePubKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None))
+ val Right(bobSig) = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), bobNonce.secretNonce, publicNonces, scriptTree_opt = None)
+ assert(Musig2.verifyTaprootSignature(bobSig, bobNonce.publicNonce, bobPubKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None))
// Once they have each other's partial signature, they can aggregate them into a valid signature.
val Right(aggregateSig) = Musig2.aggregateTaprootSignatures(Seq(aliceSig, bobSig), spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None)
@@ -56,7 +54,7 @@ class Musig2Spec extends FunSuite {
// The redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)).
// It does not depend upon the user's or server's key, just the user's refund key and the refund delay.
val redeemScript = Seq(OP_PUSHDATA(userRefundPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY)
- val scriptTree = new ScriptTree.Leaf(redeemScript.map(KotlinUtils.scala2kmp).asJava)
+ val scriptTree = ScriptTree.Leaf(redeemScript)
// The internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key.
val aggregatedKey = Musig2.aggregateKeys(Seq(userPublicKey, serverPublicKey))
@@ -80,15 +78,15 @@ class Musig2Spec extends FunSuite {
)
// The first step of a musig2 signing session is to exchange nonces.
// If participants are disconnected before the end of the signing session, they must start again with fresh nonces.
- val (userSecretNonce, userPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(userPrivateKey), Seq(userPublicKey, serverPublicKey), None, None)
- val (serverSecretNonce, serverPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Right(serverPrivateKey.publicKey), Seq(userPublicKey, serverPublicKey), None, None)
+ val userNonce = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(userPrivateKey), Seq(userPublicKey, serverPublicKey), None, None)
+ val serverNonce = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Right(serverPrivateKey.publicKey), Seq(userPublicKey, serverPublicKey), None, None)
// Once they have each other's public nonce, they can produce partial signatures.
- val publicNonces = Seq(userPublicNonce, serverPublicNonce)
- val Right(userSig) = Musig2.signTaprootInput(userPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), userSecretNonce, publicNonces, Some(scriptTree))
- assert(Musig2.verifyTaprootSignature(userSig, userPublicNonce, userPublicKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree)))
- val Right(serverSig) = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), serverSecretNonce, publicNonces, Some(scriptTree))
- assert(Musig2.verifyTaprootSignature(serverSig, serverPublicNonce, serverPublicKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree)))
+ val publicNonces = Seq(userNonce, serverNonce).map(_.publicNonce)
+ val Right(userSig) = Musig2.signTaprootInput(userPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), userNonce.secretNonce, publicNonces, Some(scriptTree))
+ assert(Musig2.verifyTaprootSignature(userSig, userNonce.publicNonce, userPublicKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree)))
+ val Right(serverSig) = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), serverNonce.secretNonce, publicNonces, Some(scriptTree))
+ assert(Musig2.verifyTaprootSignature(serverSig, serverNonce.publicNonce, serverPublicKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree)))
// Once they have each other's partial signature, they can aggregate them into a valid signature.
val Right(sig) = Musig2.aggregateTaprootSignatures(Seq(userSig, serverSig), tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree))
@@ -104,7 +102,7 @@ class Musig2Spec extends FunSuite {
txOut = Seq(TxOut(10_000 sat, Script.pay2wpkh(userPublicKey))),
lockTime = 0
)
- val sig = Transaction.signInputTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(scriptTree.hash()))
+ val sig = Transaction.signInputTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, scriptTree.hash())
val witness = Script.witnessScriptPathPay2tr(aggregatedKey, scriptTree, ScriptWitness(Seq(sig)), scriptTree)
val signedTx = tx.updateWitness(0, witness)
Transaction.correctlySpends(signedTx, Seq(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@@ -113,7 +111,8 @@ class Musig2Spec extends FunSuite {
test("generate nonce with counter") {
val sk = PrivateKey(ByteVector.fromValidHex("EEC1CB7D1B7254C5CAB0D9C61AB02E643D464A59FE6C96A7EFE871F07C5AEF54"))
- val (_, pubnonce) = Musig2.generateNonceWithCounter(0, sk, Seq(sk.publicKey), None, None)
- assert(pubnonce.getData.contentEquals(Hex.decode("0271efb262c0535e921efacacd30146fa93f193689e4974d5348fa9d909d90000702a049680ef3f6acfb12320297df31d3a634214491cbeebacef5acdf13f8f61cc2")))
+ val nonce = Musig2.generateNonceWithCounter(0, sk, Seq(sk.publicKey), None, None)
+ assert(nonce.publicNonce.data == hex"0271efb262c0535e921efacacd30146fa93f193689e4974d5348fa9d909d90000702a049680ef3f6acfb12320297df31d3a634214491cbeebacef5acdf13f8f61cc2")
}
+
}
diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala
index ae70957d..e5b5552b 100644
--- a/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala
+++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala
@@ -1,16 +1,12 @@
package fr.acinq.bitcoin.scalacompat
-import fr.acinq.bitcoin.Crypto.TaprootTweak
-import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
+import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, TaprootTweak}
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
-import fr.acinq.bitcoin.scalacompat.Transaction.hashForSigningSchnorr
-import fr.acinq.bitcoin.{Bech32, ScriptFlags, ScriptTree, SigHash, SigVersion}
+import fr.acinq.bitcoin.{Bech32, ScriptFlags, SigHash, SigVersion}
import fr.acinq.secp256k1.Secp256k1
import org.scalatest.FunSuite
import scodec.bits.ByteVector
-import scala.jdk.CollectionConverters.SeqHasAsJava
-
class TaprootSpec extends FunSuite {
test("check taproot signatures") {
@@ -19,7 +15,7 @@ class TaprootSpec extends FunSuite {
val key = DeterministicWallet.derivePrivateKey(master, "86'/1'/0'/0/1")
val internalKey = key.publicKey.xOnly
val script = Script.pay2tr(internalKey, scripts_opt = None)
- val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak.INSTANCE)
+ val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak)
assert("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c" == Bech32.encodeWitnessAddress("tb", 1, outputKey.pub.value.toByteArray))
assert(script == Script.pay2tr(outputKey))
@@ -45,23 +41,23 @@ class TaprootSpec extends FunSuite {
assert(Crypto.verifySignatureSchnorr(hash, sig, outputKey))
// re-create signature
- val ourSig = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak.INSTANCE)
+ val ourSig = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak)
assert(Crypto.verifySignatureSchnorr(hash, ourSig, outputKey))
assert(Secp256k1.get().verifySchnorr(ourSig.toArray, hash.toArray, outputKey.pub.value.toByteArray))
// setting auxiliary random data to all-zero yields the same result as not setting any auxiliary random data
- val ourSig1 = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak.INSTANCE, Some(ByteVector32.Zeroes))
+ val ourSig1 = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak, Some(ByteVector32.Zeroes))
assert(ourSig == ourSig1)
// setting auxiliary random data to a non-zero value yields a different result
- val ourSig2 = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak.INSTANCE, Some(ByteVector32.One))
+ val ourSig2 = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak, Some(ByteVector32.One))
assert(ourSig != ourSig2)
}
test("send to and spend from taproot addresses") {
val privateKey = PrivateKey(ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101"))
val internalKey = privateKey.publicKey.xOnly
- val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak.INSTANCE)
+ val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak)
val address = Bech32.encodeWitnessAddress("tb", 1, outputKey.pub.value.toByteArray)
assert("tb1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8snwrkwy" == address)
@@ -156,7 +152,7 @@ class TaprootSpec extends FunSuite {
)
// simple script tree with a single element
- val scriptTree = new ScriptTree.Leaf(script.map(scala2kmp).asJava)
+ val scriptTree = ScriptTree.Leaf(script)
// we choose a pubkey that does not have a corresponding private key: our funding tx can only be spent through the script path, not the key path
val internalPubkey = PublicKey.fromBin(ByteVector.fromValidHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")).xOnly
@@ -201,13 +197,13 @@ class TaprootSpec extends FunSuite {
PrivateKey(ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010103"))
)
val scripts: Seq[Seq[ScriptElt]] = privs.map { p => Seq(OP_PUSHDATA(p.xOnlyPublicKey()), OP_CHECKSIG) }
- val leaves = scripts.map { script => new ScriptTree.Leaf(script.map(scala2kmp).asJava) }
+ val leaves = scripts.map(ScriptTree.Leaf(_))
// root
// / \
// / \ #3
// #1 #2
- val scriptTree = new ScriptTree.Branch(
- new ScriptTree.Branch(leaves(0), leaves(1)),
+ val scriptTree = ScriptTree.Branch(
+ ScriptTree.Branch(leaves(0), leaves(1)),
leaves(2)
)
val blockchain = Block.SignetGenesisBlock.hash