Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<groupId>fr.acinq</groupId>
<artifactId>bitcoin-lib_2.13</artifactId>
<packaging>jar</packaging>
<version>0.43</version>
<version>0.43.1</version>
<description>Simple Scala Bitcoin library</description>
<url>https://github.com/ACINQ/bitcoin-lib</url>
<name>bitcoin-lib</name>
Expand Down
35 changes: 28 additions & 7 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -124,24 +145,24 @@ 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
*
* @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
Expand Down Expand Up @@ -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)
}

/**
Expand Down
45 changes: 44 additions & 1 deletion src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 }

}

50 changes: 34 additions & 16 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
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(secret: SecretNonce, public: 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
* the public key exposed in the script (which is tweaked with the script tree).
*
* @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))
}

/**
Expand All @@ -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))
}

/**
Expand All @@ -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)
}

/**
Expand All @@ -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)
}

/**
Expand All @@ -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)
}

}
4 changes: 2 additions & 2 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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))

}
56 changes: 56 additions & 0 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/ScriptTree.scala
Original file line number Diff line number Diff line change
@@ -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
}
Loading