Skip to content

Commit

Permalink
Provide high-level taproot abstractions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Jan 17, 2024
1 parent 185abcb commit 603efa4
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 75 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
<dependency>
<groupId>fr.acinq.bitcoin</groupId>
<artifactId>bitcoin-kmp-jvm</artifactId>
<version>0.15.0</version>
<version>0.15.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
Expand Down
18 changes: 18 additions & 0 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import fr.acinq.bitcoin
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
import scodec.bits.ByteVector

import scala.jdk.CollectionConverters.SeqHasAsJava

object Crypto {
/**
* A bitcoin private key.
Expand Down Expand Up @@ -137,6 +139,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 Expand Up @@ -272,6 +280,16 @@ object Crypto {
bitcoin.Crypto.signSchnorr(data, privateKey, schnorrTweak, auxrand32.map(scala2kmp).orNull)
}

/** Produce a signature that will be included in the witness of a taproot key path spend. */
def signTaprootKeyPath(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.Crypto.signTaprootKeyPath(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, scriptTree_opt.orNull, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull)
}

/** Produce a signature that will be included in the witness of a taproot script path spend. */
def signTaprootScriptPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], sighashType: Int, tapleaf: ByteVector32, annex_opt: Option[ByteVector] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
bitcoin.Crypto.signTaprootScriptPath(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, sighashType, tapleaf, annex_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull)
}

/**
* Recover public keys from a signature and the message that was signed. This method will return 2 public keys, and the signature
* can be verified with both, but only one of them matches that private key that was used to generate the signature.
Expand Down
25 changes: 24 additions & 1 deletion src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,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)

}
28 changes: 20 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
Loading

0 comments on commit 603efa4

Please sign in to comment.