Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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(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
* 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