Skip to content

Commit

Permalink
Provide high-level taproot and musig2 abstractions (#85)
Browse files Browse the repository at this point in the history
* Provide high-level taproot abstractions

We provide helpers for spending taproot output via the key path or any
script path, without dealing with low-level details such as signature
version, control blocks or script execution context.

It makes it easier and less error-prone to spend taproot outputs in
higher level applications.

* Add high-level helpers for using Musig2 with Taproot

When using Musig2 for a taproot key path, we can provide simpler helper
functions to collaboratively build a shared signature for the spending
transaction.

This hides all of the low-level details of how the musig2 algorithm
works, by exposing a subset of what can be done that is sufficient for
spending taproot inputs.

* Remove Script ExecutionData

This is an internal detail that shouldn't be exposed.

* Add kotlin/scala converter for XonlyPublicKey

---------

Co-authored-by: sstone <fabrice@acinq.fr>
  • Loading branch information
t-bast and sstone authored Feb 14, 2024
1 parent 185abcb commit 236b057
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 102 deletions.
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

0 comments on commit 236b057

Please sign in to comment.