Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide high-level taproot and musig2 abstractions #85

Merged
merged 7 commits into from
Feb 14, 2024
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
8 changes: 4 additions & 4 deletions 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.32-SNAPSHOT</version>
<version>0.32-MUSIG2-SNAPSHOT</version>
<description>Simple Scala Bitcoin library</description>
<url>https://github.com/ACINQ/bitcoin-lib</url>
<name>bitcoin-lib</name>
Expand Down Expand Up @@ -171,17 +171,17 @@
<dependency>
<groupId>fr.acinq.bitcoin</groupId>
<artifactId>bitcoin-kmp-jvm</artifactId>
<version>0.15.0</version>
<version>0.17.0</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
<artifactId>secp256k1-kmp-jni-jvm</artifactId>
<version>0.12.0</version>
<version>0.14.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>1.8.21</version>
<version>1.9.22</version>
</dependency>
<dependency>
<groupId>org.scodec</groupId>
Expand Down
6 changes: 6 additions & 0 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ object Crypto {
(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))

/** Tweak this key with the merkle root provided. */
def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(new bitcoin.Crypto.TaprootTweak.ScriptTweak(merkleRoot))

/**
* add a public key to this x-only key
*
Expand Down
15 changes: 7 additions & 8 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fr.acinq.bitcoin.scalacompat

import fr.acinq.bitcoin
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
import scodec.bits.ByteVector

import java.io.{InputStream, OutputStream}
Expand Down Expand Up @@ -62,11 +62,15 @@ object KotlinUtils {

implicit def kmp2scala(input: bitcoin.PrivateKey): PrivateKey = PrivateKey(input)

implicit def scala2kmp(input: PrivateKey): bitcoin.PrivateKey = new bitcoin.PrivateKey(input.value)
implicit def scala2kmp(input: PrivateKey): bitcoin.PrivateKey = input.priv

implicit def kmp2scala(input: bitcoin.PublicKey): PublicKey = PublicKey(input)

implicit def scala2kmp(input: PublicKey): bitcoin.PublicKey = new bitcoin.PublicKey(input.value)
implicit def scala2kmp(input: PublicKey): bitcoin.PublicKey = input.pub

implicit def kmp2scala(input: bitcoin.XonlyPublicKey): XonlyPublicKey = XonlyPublicKey(input)

implicit def scala2kmp(input: XonlyPublicKey): bitcoin.XonlyPublicKey = input.pub

implicit def kmp2scala(input: bitcoin.DeterministicWallet.ExtendedPrivateKey): DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.ExtendedPrivateKey(input)

Expand All @@ -80,11 +84,6 @@ object KotlinUtils {

implicit def scala2kmp(input: DeterministicWallet.KeyPath): bitcoin.KeyPath = input.keyPath

implicit def scala2kmp(input: Script.ExecutionData): bitcoin.Script.ExecutionData =
new bitcoin.Script.ExecutionData(input.annex.map(scala2kmp).orNull, input.tapleafHash.map(scala2kmp).orNull, input.validationWeightLeft.map(i => Integer.valueOf(i)).orNull, input.codeSeparatorPos)

implicit def kmp2scala(input: bitcoin.Script.ExecutionData): Script.ExecutionData = Script.ExecutionData(Option(input.getAnnex), Option(input.getTapleafHash), Option(input.getValidationWeightLeft), input.getCodeSeparatorPos)

case class InputStreamWrapper(is: InputStream) extends bitcoin.io.Input {
// NB: on the JVM we will use a ByteArrayInputStream, which guarantees that the result will be correct.
override def getAvailableBytes: Int = is.available()
Expand Down
62 changes: 62 additions & 0 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package fr.acinq.bitcoin.scalacompat

import fr.acinq.bitcoin.ScriptTree
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
import fr.acinq.bitcoin.scalacompat.KotlinUtils._

import scala.jdk.CollectionConverters.SeqHasAsJava

object Musig2 {

/**
* 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))

/**
* @param sessionId a random, unique session ID.
* @param privateKey signer's private key.
* @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
*/
def generateNonce(sessionId: ByteVector32, privateKey: PrivateKey, publicKeys: Seq[PublicKey]): (SecretNonce, IndividualNonce) = {
val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, privateKey, publicKeys.map(scala2kmp).asJava)
(nonce.getFirst, nonce.getSecond)
}

/**
* Create a partial musig2 signature for the given taproot input key path.
*
* @param privateKey private key of the signing participant.
* @param tx transaction spending the target taproot input.
* @param inputIndex index of the taproot input to spend.
* @param inputs all inputs of the spending transaction.
* @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid.
* @param secretNonce secret nonce of the signing participant.
* @param publicNonces public nonces of all participants of the musig2 session.
* @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)
}

/**
* Aggregate partial musig2 signatures into a valid schnorr signature for the given taproot input key path.
*
* @param partialSigs partial musig2 signatures of all participants of the musig2 session.
* @param tx transaction spending the target taproot input.
* @param inputIndex index of the taproot input to spend.
* @param inputs all inputs of the spending transaction.
* @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid.
* @param publicNonces public nonces of all participants of the musig2 session.
* @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)
}

}
31 changes: 24 additions & 7 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,6 @@ object Script {
require(inputIndex >= 0 && inputIndex < tx.txIn.length, "invalid input index")
}

case class ExecutionData(annex: Option[ByteVector], tapleafHash: Option[ByteVector32], validationWeightLeft: Option[Int] = None, codeSeparatorPos: Long = 0xFFFFFFFFL)

object ExecutionData {
val empty: ExecutionData = ExecutionData(None, None)
}

/**
* Bitcoin script runner
*
Expand Down Expand Up @@ -171,6 +165,29 @@ object Script {
*/
def witnessPay2wpkh(pubKey: PublicKey, sig: ByteVector): ScriptWitness = bitcoin.Script.witnessPay2wpkh(pubKey, sig)

def pay2tr(publicKey: XonlyPublicKey): Seq[ScriptElt] = bitcoin.Script.pay2tr(publicKey.pub).asScala.map(kmp2scala).toList
/**
* @param outputKey public key exposed by the taproot script (tweaked based on the tapscripts).
* @return a pay-to-taproot script.
*/
def pay2tr(outputKey: XonlyPublicKey): Seq[ScriptElt] = bitcoin.Script.pay2tr(outputKey.pub).asScala.map(kmp2scala).toList

/**
* @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 isPay2tr(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPay2tr(script.map(scala2kmp).asJava)

/** NB: callers must ensure that they use the correct taproot tweak when generating their signature. */
def witnessKeyPathPay2tr(sig: ByteVector64, sighash: Int = bitcoin.SigHash.SIGHASH_DEFAULT): ScriptWitness = bitcoin.Script.witnessKeyPathPay2tr(sig, sighash)

/**
* @param internalKey taproot internal public key.
* @param script script that is spent (must exist in the [scriptTree]).
* @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)

}
58 changes: 50 additions & 8 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,15 +248,27 @@ object Transaction extends BtcSerializer[Transaction] {
hashForSigning(tx, inputIndex, Script.write(previousOutputScript), sighashType, amount, signatureVersion)

/**
* @param tx transaction to sign
* @param inputIndex index of the transaction input being signed
* @param inputs UTXOs spent by this transaction
* @param sighashType signature hash type
* @param sigVersion signature version
* @param executionData execution context of a transaction script
* @param tx transaction to sign
* @param inputIndex index of the transaction input being signed
* @param inputs UTXOs spent by this transaction
* @param sighashType signature hash type
* @param sigVersion signature version
* @param tapleaf_opt when spending a tapscript, the hash of the corresponding script leaf must be provided
* @param annex_opt (optional) taproot annex
*/
def hashForSigningSchnorr(tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, sigVersion: Int, executionData: Script.ExecutionData = Script.ExecutionData.empty): ByteVector32 =
bitcoin.Transaction.hashForSigningSchnorr(tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, sigVersion, executionData)
def hashForSigningSchnorr(tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, sigVersion: Int, tapleaf_opt: Option[ByteVector32] = None, annex_opt: Option[ByteVector] = None): ByteVector32 = {
bitcoin.Transaction.hashForSigningSchnorr(tx, 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(tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, annex_opt: Option[ByteVector] = None): ByteVector32 = {
bitcoin.Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, annex_opt.map(scala2kmp).orNull)
}

/** Use this function when spending a taproot script path. */
def hashForSigningTaprootScriptPath(tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None): ByteVector32 = {
bitcoin.Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scala2kmp(tapleaf), annex_opt.map(scala2kmp).orNull)
}

/**
* sign a tx input
Expand Down Expand Up @@ -289,6 +301,36 @@ object Transaction extends BtcSerializer[Transaction] {
def signInput(tx: Transaction, inputIndex: Int, previousOutputScript: Seq[ScriptElt], sighashType: Int, amount: Satoshi, signatureVersion: Int, privateKey: PrivateKey): ByteVector =
signInput(tx, inputIndex, Script.write(previousOutputScript), sighashType, amount, signatureVersion, privateKey)

/**
* Sign a taproot tx input, using the internal key path.
*
* @param privateKey private key.
* @param tx input transaction.
* @param inputIndex index of the tx input that is being signed.
* @param inputs list of all UTXOs spent by this transaction.
* @param sighashType signature hash type, which will be appended to the signature (if not default).
* @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 = {
bitcoin.Transaction.signInputTaprootKeyPath(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull)
}

/**
* Sign a taproot tx input, using one of its script paths.
*
* @param privateKey private key.
* @param tx input transaction.
* @param inputIndex index of the tx input that is being signed.
* @param inputs list of all UTXOs spent by this transaction.
* @param sighashType signature hash type, which will be appended to the signature (if not default).
* @param tapleaf tapscript leaf hash of the script that is being spent.
* @return the schnorr signature of this tx for this specific tx input and the given script leaf.
*/
def signInputTaprootScriptPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
bitcoin.Transaction.signInputTaprootScriptPath(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, tapleaf, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull)
}

def correctlySpends(tx: Transaction, previousOutputs: Map[OutPoint, TxOut], scriptFlags: Int): Unit = {
fr.acinq.bitcoin.Transaction.correctlySpends(tx, previousOutputs.map { case (o, t) => scala2kmp(o) -> scala2kmp(t) }.asJava, scriptFlags)
}
Expand Down
14 changes: 4 additions & 10 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,16 @@ package object scalacompat {
* @param script public key script
* @return the address of this public key script on this chain
*/
def computeScriptAddress(chainHash: BlockHash, script: Seq[ScriptElt]): Either[AddressFromPublicKeyScriptResult.Failure, String] = addressFromPublicKeyScript(chainHash, script)
def computeScriptAddress(chainHash: BlockHash, script: Seq[ScriptElt]): Either[BitcoinError, String] = addressFromPublicKeyScript(chainHash, script)

/**
* @param chainHash hash of the chain (i.e. hash of the genesis block of the chain we're on)
* @param script public key script
* @return the address of this public key script on this chain
*/
def computeScriptAddress(chainHash: BlockHash, script: ByteVector): Either[AddressFromPublicKeyScriptResult.Failure, String] = computeScriptAddress(chainHash, Script.parse(script))
def computeScriptAddress(chainHash: BlockHash, script: ByteVector): Either[BitcoinError, String] = computeScriptAddress(chainHash, Script.parse(script))

def addressToPublicKeyScript(chainHash: BlockHash, address: String): Either[AddressToPublicKeyScriptResult.Failure, Seq[ScriptElt]] = fr.acinq.bitcoin.Bitcoin.addressToPublicKeyScript(chainHash, address) match {
case success: AddressToPublicKeyScriptResult.Success => Right(success.getResult.asScala.map(kmp2scala).toList)
case failure: AddressToPublicKeyScriptResult.Failure => Left(failure)
}
def addressToPublicKeyScript(chainHash: BlockHash, address: String): Either[BitcoinError, Seq[ScriptElt]] = fr.acinq.bitcoin.Bitcoin.addressToPublicKeyScript(chainHash, address).map(_.asScala.map(kmp2scala).toList)

def addressFromPublicKeyScript(chainHash: BlockHash, script: Seq[ScriptElt]): Either[AddressFromPublicKeyScriptResult.Failure, String] = fr.acinq.bitcoin.Bitcoin.addressFromPublicKeyScript(chainHash, script.map(scala2kmp).asJava) match {
case success: AddressFromPublicKeyScriptResult.Success => Right(success.getAddress)
case failure: AddressFromPublicKeyScriptResult.Failure => Left(failure)
}
def addressFromPublicKeyScript(chainHash: BlockHash, script: Seq[ScriptElt]): Either[BitcoinError, String] = fr.acinq.bitcoin.Bitcoin.addressFromPublicKeyScript(chainHash, script.map(scala2kmp).asJava)
}
Loading
Loading