From 236b057375566c8c22cf52fba41ec2b823a13e58 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:18:29 +0100 Subject: [PATCH] Provide high-level taproot and musig2 abstractions (#85) * 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 --- pom.xml | 8 +- .../fr/acinq/bitcoin/scalacompat/Crypto.scala | 6 + .../bitcoin/scalacompat/KotlinUtils.scala | 15 ++- .../fr/acinq/bitcoin/scalacompat/Musig2.scala | 62 ++++++++++ .../fr/acinq/bitcoin/scalacompat/Script.scala | 31 +++-- .../bitcoin/scalacompat/Transaction.scala | 58 +++++++-- .../acinq/bitcoin/scalacompat/package.scala | 14 +-- .../bitcoin/scalacompat/Musig2Spec.scala | 109 +++++++++++++++++ .../bitcoin/scalacompat/TaprootSpec.scala | 111 ++++++++---------- 9 files changed, 312 insertions(+), 102 deletions(-) create mode 100644 src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala create mode 100644 src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala diff --git a/pom.xml b/pom.xml index 2080e6d5..f68e0eda 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ fr.acinq bitcoin-lib_2.13 jar - 0.32-SNAPSHOT + 0.32-MUSIG2-SNAPSHOT Simple Scala Bitcoin library https://github.com/ACINQ/bitcoin-lib bitcoin-lib @@ -171,17 +171,17 @@ fr.acinq.bitcoin bitcoin-kmp-jvm - 0.15.0 + 0.17.0 fr.acinq.secp256k1 secp256k1-kmp-jni-jvm - 0.12.0 + 0.14.0 org.jetbrains.kotlin kotlin-stdlib-jdk8 - 1.8.21 + 1.9.22 org.scodec diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala index 32d2b66a..660b8a57 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala @@ -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 * diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala index 0397196e..f575df8c 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala @@ -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} @@ -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) @@ -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() diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala new file mode 100644 index 00000000..ed30666c --- /dev/null +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala @@ -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) + } + +} diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala index 44d62bbe..ab914ef0 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala @@ -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 * @@ -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) } diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala index 63d0c4be..1d2e9f9a 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Transaction.scala @@ -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 @@ -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) } diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/package.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/package.scala index 5edaf7b4..d6c71865 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/package.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/package.scala @@ -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) } diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala new file mode 100644 index 00000000..57f9fb08 --- /dev/null +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala @@ -0,0 +1,109 @@ +package fr.acinq.bitcoin.scalacompat + +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash} +import org.scalatest.FunSuite +import scodec.bits.{ByteVector, HexStringSyntax} + +import scala.jdk.CollectionConverters.SeqHasAsJava +import scala.util.Random + +class Musig2Spec extends FunSuite { + + test("use musig2 to replace multisig 2-of-2") { + val alicePrivKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") + val alicePubKey = alicePrivKey.publicKey + val bobPrivKey = PrivateKey(hex"0202020202020202020202020202020202020202020202020202020202020202") + val bobPubKey = bobPrivKey.publicKey + + // Alice and Bob exchange public keys and agree on a common aggregated key. + val internalPubKey = Musig2.aggregateKeys(Seq(alicePubKey, bobPubKey)) + + // This tx sends to a taproot script that doesn't contain any script path. + val tx = Transaction(2, Nil, Seq(TxOut(10_000 sat, Script.pay2tr(internalPubKey, scripts_opt = None))), 0) + // This tx spends the previous tx with Alice and Bob's signatures. + val spendingTx = Transaction(2, Seq(TxIn(OutPoint(tx, 0), ByteVector.empty, 0)), Seq(TxOut(10_000 sat, Script.pay2wpkh(alicePubKey))), 0) + + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val (aliceSecretNonce, alicePublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), alicePrivKey, Seq(alicePubKey, bobPubKey)) + val (bobSecretNonce, bobPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), bobPrivKey, Seq(alicePubKey, bobPubKey)) + + // Once they have each other's public nonce, they can produce partial signatures. + val publicNonces = Seq(alicePublicNonce, bobPublicNonce) + val Right(aliceSig) = Musig2.signTaprootInput(alicePrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), aliceSecretNonce, publicNonces, scriptTree_opt = None) + val Right(bobSig) = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), bobSecretNonce, publicNonces, scriptTree_opt = None) + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val Right(aggregateSig) = Musig2.aggregateTaprootSignatures(Seq(aliceSig, bobSig), spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None) + + // This tx looks like any other tx that spends a p2tr output, with a single signature. + val signedSpendingTx = spendingTx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregateSig)) + Transaction.correctlySpends(signedSpendingTx, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + test("swap-in-potentiam example with musig2 and taproot") { + val userPrivateKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") + val userPublicKey = userPrivateKey.publicKey + val serverPrivateKey = PrivateKey(hex"0202020202020202020202020202020202020202020202020202020202020202") + val serverPublicKey = serverPrivateKey.publicKey + val userRefundPrivateKey = PrivateKey(hex"0303030303030303030303030303030303030303030303030303030303030303") + val refundDelay = 25920 + + // The redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)). + // It does not depend upon the user's or server's key, just the user's refund key and the refund delay. + val redeemScript = Seq(OP_PUSHDATA(userRefundPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + val scriptTree = new ScriptTree.Leaf(0, redeemScript.map(KotlinUtils.scala2kmp).asJava) + + // The internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key. + val aggregatedKey = Musig2.aggregateKeys(Seq(userPublicKey, serverPublicKey)) + // It is tweaked with the script's merkle root to get the pubkey that will be exposed. + val pubkeyScript = Script.pay2tr(aggregatedKey, Some(scriptTree)) + + val swapInTx = Transaction( + version = 2, + txIn = Nil, + txOut = Seq(TxOut(10_000 sat, pubkeyScript)), + lockTime = 0 + ) + + // The transaction can be spent if the user and the server produce a signature. + { + val tx = Transaction( + version = 2, + txIn = Seq(TxIn(OutPoint(swapInTx, 0), ByteVector.empty, 0xFFFFFFFD)), + txOut = Seq(TxOut(10_000 sat, Script.pay2wpkh(userPublicKey))), + lockTime = 0 + ) + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val (userSecretNonce, userPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), userPrivateKey, Seq(userPublicKey, serverPublicKey)) + val (serverSecretNonce, serverPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), serverPrivateKey, Seq(userPublicKey, serverPublicKey)) + + // Once they have each other's public nonce, they can produce partial signatures. + val publicNonces = Seq(userPublicNonce, serverPublicNonce) + val Right(userSig) = Musig2.signTaprootInput(userPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), userSecretNonce, publicNonces, Some(scriptTree)) + val Right(serverSig) = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), serverSecretNonce, publicNonces, Some(scriptTree)) + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val Right(sig) = Musig2.aggregateTaprootSignatures(Seq(userSig, serverSig), tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree)) + val signedTx = tx.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) + Transaction.correctlySpends(signedTx, Seq(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // Or it can be spent with only the user's signature, after a delay. + { + val tx = Transaction( + version = 2, + txIn = Seq(TxIn(OutPoint(swapInTx, 0), ByteVector.empty, refundDelay)), + txOut = Seq(TxOut(10_000 sat, Script.pay2wpkh(userPublicKey))), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(scriptTree.hash())) + val witness = Script.witnessScriptPathPay2tr(aggregatedKey, scriptTree, ScriptWitness(Seq(sig)), scriptTree) + val signedTx = tx.updateWitness(0, witness) + Transaction.correctlySpends(signedTx, Seq(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + } + +} diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala index 31f1d1f1..ecaab903 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala @@ -1,28 +1,34 @@ package fr.acinq.bitcoin.scalacompat import fr.acinq.bitcoin.Crypto.TaprootTweak -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.bitcoin.scalacompat.Transaction.hashForSigningSchnorr -import fr.acinq.bitcoin.{Bech32, ScriptFlags, ScriptLeaf, ScriptTree, SigHash, SigVersion} +import fr.acinq.bitcoin.{Bech32, ScriptFlags, ScriptTree, SigHash, SigVersion} import fr.acinq.secp256k1.Secp256k1 import org.scalatest.FunSuite import scodec.bits.ByteVector +import scala.jdk.CollectionConverters.SeqHasAsJava + class TaprootSpec extends FunSuite { + test("check taproot signatures") { // derive BIP86 wallet key val (_, master) = DeterministicWallet.ExtendedPrivateKey.decode("tprv8ZgxMBicQKsPeQQADibg4WF7mEasy3piWZUHyThAzJCPNgMHDVYhTCVfev3jFbDhcYm4GimeFMbbi9z1d9rfY1aL5wfJ9mNebQ4thJ62EJb") val key = DeterministicWallet.derivePrivateKey(master, "86'/1'/0'/0/1") - val internalKey = XonlyPublicKey(key.publicKey) + val internalKey = key.publicKey.xOnly + val script = Script.pay2tr(internalKey, scripts_opt = None) val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak.INSTANCE) assert("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c" == Bech32.encodeWitnessAddress("tb", 1, outputKey.pub.value.toByteArray)) + assert(script == Script.pay2tr(outputKey)) // tx sends to tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c val tx = Transaction.read( "02000000000101590c995983abb86d8196f57357f2aac0e6cc6144d8239fd8a171810b476269d50000000000feffffff02a086010000000000225120bfef0f753700ac863e748f8d02c4b0d1fc7569933fd55fb6c3c598e84ff28b7c13d3abe65a060000160014353b5487959c58f5feafe63800057899f9ece4280247304402200b20c43175358c970850a583fd60d36c06588f1103b82b0968dc21e20e7d7958022027c64923623205c4985541d4a9fc6b5df4111d918fe63803337538b029c17ea20121022f685476d299e7b49d3a6b380e10aec1f93d96819fd7697669fabb533cc052624ff50000" ) - assert(Script.pay2tr(outputKey) == Script.parse(tx.txOut.head.publicKeyScript)) + assert(Script.isPay2tr(Script.parse(tx.txOut.head.publicKeyScript))) + assert(script == Script.parse(tx.txOut.head.publicKeyScript)) // tx1 spends tx using key path spending i.e its witness just includes a single signature that is valid for outputKey val tx1 = Transaction.read( @@ -54,7 +60,7 @@ class TaprootSpec extends FunSuite { test("send to and spend from taproot addresses") { val privateKey = PrivateKey(ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101")) - val internalKey = XonlyPublicKey(privateKey.publicKey) + val internalKey = privateKey.publicKey.xOnly val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak.INSTANCE) val address = Bech32.encodeWitnessAddress("tb", 1, outputKey.pub.value.toByteArray) assert("tb1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8snwrkwy" == address) @@ -63,7 +69,7 @@ class TaprootSpec extends FunSuite { val tx = Transaction.read( "02000000000101bf77ef36f2c0f32e0822cef0514948254997495a34bfba7dd4a73aabfcbb87900000000000fdffffff02c2c2000000000000160014b5c3dbfeb8e7d0c809c3ba3f815fd430777ef4be50c30000000000002251208c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f0140583f758bea307216e03c1f54c3c6088e8923c8e1c89d96679fb00de9e808a79d0fba1cc3f9521cb686e8f43fb37cc6429f2e1480c70cc25ecb4ac0dde8921a01f1f70000" ) - assert(Script.pay2tr(outputKey) == Script.parse(tx.txOut(1).publicKeyScript)) + assert(Script.pay2tr(internalKey, scripts_opt = None) == Script.parse(tx.txOut(1).publicKeyScript)) // we want to spend val Right(outputScript) = addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, "tb1pn3g330w4n5eut7d4vxq0pp303267qc6vg8d2e0ctjuqre06gs3yqnc5yx0") @@ -74,9 +80,8 @@ class TaprootSpec extends FunSuite { 0 ) val sigHashType = 0 - val hash = hashForSigningSchnorr(tx1, 0, tx.txOut(1) :: Nil, sigHashType, 0) - val sig = Crypto.signSchnorr(hash, privateKey, TaprootTweak.NoScriptTweak.INSTANCE) - val tx2 = tx1.updateWitness(0, ScriptWitness(sig :: Nil)) + val sig = Transaction.signInputTaprootKeyPath(privateKey, tx1, 0, tx.txOut(1) :: Nil, sigHashType, scriptTree_opt = None) + val tx2 = tx1.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) Transaction.correctlySpends(tx2, tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -151,15 +156,12 @@ class TaprootSpec extends FunSuite { ) // simple script tree with a single element - val scriptTree = new ScriptTree.Leaf(new ScriptLeaf(0, Script.write(script), fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT)) - val merkleRoot = ScriptTree.hash(scriptTree) - + val scriptTree = new ScriptTree.Leaf(0, script.map(scala2kmp).asJava) // we choose a pubkey that does not have a corresponding private key: our funding tx can only be spent through the script path, not the key path - val internalPubkey = XonlyPublicKey(PublicKey.fromBin(ByteVector.fromValidHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"))) - val (tweakedKey, parity) = internalPubkey.outputKey(new fr.acinq.bitcoin.Crypto.TaprootTweak.ScriptTweak(merkleRoot)) + val internalPubkey = PublicKey.fromBin(ByteVector.fromValidHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")).xOnly // funding tx sends to our tapscript - val fundingTx = Transaction(version = 2, txIn = Nil, txOut = Seq(TxOut(Satoshi(1000000), Script.pay2tr(tweakedKey))), lockTime = 0) + val fundingTx = Transaction(version = 2, txIn = Nil, txOut = Seq(TxOut(Satoshi(1000000), Script.pay2tr(internalPubkey, Some(scriptTree)))), lockTime = 0) // create an unsigned transaction val tmp = Transaction( @@ -168,30 +170,26 @@ class TaprootSpec extends FunSuite { txOut = TxOut(fundingTx.txOut.head.amount - Satoshi(5000), addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, "bcrt1qdtu5cwyngza8hw8s5uk2erlrkh8ceh3msp768v").toOption.get) :: Nil, lockTime = 0 ) - val hash = hashForSigningSchnorr(tmp, 0, Seq(fundingTx.txOut.head), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(annex = None, tapleafHash = Some(merkleRoot))) // compute all 3 signatures - val sigs = privs.map { p => Crypto.signSchnorr(hash, p, fr.acinq.bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE) } - - // control is the same for everyone since there are no specific merkle hashes to provide - val controlBlock = ByteVector.view((fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte +: internalPubkey.pub.value.toByteArray) + val sigs = privs.map { p => Transaction.signInputTaprootScriptPath(p, tmp, 0, Seq(fundingTx.txOut.head), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) } // one signature is not enough - val tx = tmp.updateWitness(0, ScriptWitness(Seq(sigs.head, sigs.head, sigs.head, Script.write(script), controlBlock))) + val tx = tmp.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubkey, scriptTree, ScriptWitness(Seq(sigs(0), sigs(0), sigs(0))), scriptTree)) intercept[RuntimeException] { Transaction.correctlySpends(tx, fundingTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // spend with sigs #0 and #1 - val tx1 = tmp.updateWitness(0, ScriptWitness(Seq(ByteVector.empty, sigs(1), sigs.head, Script.write(script), controlBlock))) + val tx1 = tmp.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubkey, scriptTree, ScriptWitness(Seq(ByteVector.empty, sigs(1), sigs(0))), scriptTree)) Transaction.correctlySpends(tx1, fundingTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // spend with sigs #0 and #2 - val tx2 = tmp.updateWitness(0, ScriptWitness(Seq(sigs(2), ByteVector.empty, sigs.head, Script.write(script), controlBlock))) + val tx2 = tmp.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubkey, scriptTree, ScriptWitness(Seq(sigs(2), ByteVector.empty, sigs(0))), scriptTree)) Transaction.correctlySpends(tx2, fundingTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // spend with sigs #0, #1 and #2 - val tx3 = tmp.updateWitness(0, ScriptWitness(Seq(sigs(2), sigs(1), sigs.head, Script.write(script), controlBlock))) + val tx3 = tmp.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubkey, scriptTree, ScriptWitness(Seq(sigs(2), sigs(1), sigs(0))), scriptTree)) Transaction.correctlySpends(tx3, fundingTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -202,26 +200,24 @@ class TaprootSpec extends FunSuite { PrivateKey(ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010102")), PrivateKey(ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010103")) ) - val scripts: Seq[Seq[ScriptElt]] = privs.map { p => Seq(OP_PUSHDATA(XonlyPublicKey(p.publicKey())), OP_CHECKSIG) } - - val leaves = scripts.zipWithIndex.map { case (script, idx) => new ScriptTree.Leaf(new ScriptLeaf(idx, Script.write(script), fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT)) } + val scripts: Seq[Seq[ScriptElt]] = privs.map { p => Seq(OP_PUSHDATA(p.xOnlyPublicKey()), OP_CHECKSIG) } + val leaves = scripts.zipWithIndex.map { case (script, idx) => new ScriptTree.Leaf(idx, script.map(scala2kmp).asJava) } // root // / \ // / \ #3 // #1 #2 val scriptTree = new ScriptTree.Branch( - new ScriptTree.Branch(leaves.head, leaves(1)), + new ScriptTree.Branch(leaves(0), leaves(1)), leaves(2) ) - val merkleRoot = ScriptTree.hash(scriptTree) val blockchain = Block.SignetGenesisBlock.hash // we use key #1 as our internal key - val internalPubkey = XonlyPublicKey(privs.head.publicKey()) - val (tweakedKey, parity) = internalPubkey.outputKey(new fr.acinq.bitcoin.Crypto.TaprootTweak.ScriptTweak(merkleRoot)) + val internalPubkey = privs.head.xOnlyPublicKey() + val (tweakedKey, _) = internalPubkey.outputKey(scriptTree) // this is the tapscript we send funds to - val script = Script.write(Seq(OP_1, OP_PUSHDATA(tweakedKey))) + val script = Script.pay2tr(internalPubkey, Some(scriptTree)) val bip350Address = Bech32.encodeWitnessAddress(Bech32.hrp(blockchain), 1.toByte, tweakedKey.pub.value.toByteArray) assert(bip350Address == "tb1p78gx95syx0qz8w5nftk8t7nce78zlpqpsxugcvq5xpfy4tvn6rasd7wk0y") val Right(sweepPublicKeyScript) = addressToPublicKeyScript(blockchain, "tb1qxy9hhxkw7gt76qrm4yzw4j06gkk4evryh8ayp7") @@ -230,8 +226,8 @@ class TaprootSpec extends FunSuite { val fundingTx = Transaction.read("020000000001017034061243a7770f791aa2afdb118be900f4f8fc755a36d8632213acc139bab20100000000feffffff0200e1f50500000000225120f1d062d20433c023ba934aec75fa78cf8e2f840181b88c301430524aad93d0fbc192ac1700000000160014b66f2e807b9f4adecb99ad811dde501ca3f0fd5f02473044022046a2fd077e12b1d7ba74f6e7ac469deb3e3755c100216abad667980fc39463dc022018b63cfaf72fde0b5ca10c617aeaa0015013bd06ef08f82eea500c6467d963cc0121030b50ec81d958ae79d34d3579faf72456213d7d581a908e2b64d21b96777882043ab10100") // output #1 is the one we want to spend - assert(fundingTx.txOut.head.publicKeyScript == script) - assert(addressToPublicKeyScript(blockchain, bip350Address) == Right(Seq(OP_1, OP_PUSHDATA(tweakedKey)))) + assert(fundingTx.txOut.head.publicKeyScript == Script.write(script)) + assert(addressToPublicKeyScript(blockchain, bip350Address) == Right(script)) // spending with the key path: no need to provide any script val tx = { @@ -241,10 +237,9 @@ class TaprootSpec extends FunSuite { txOut = Seq(TxOut(fundingTx.txOut.head.amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - val hash = hashForSigningSchnorr(tmp, 0, Seq(fundingTx.txOut.head), SigHash.SIGHASH_DEFAULT, 0) - // we still need to know the merkle root of the tapscript tree - val sig = Crypto.signSchnorr(hash, privs.head, new fr.acinq.bitcoin.Crypto.TaprootTweak.ScriptTweak(merkleRoot)) - tmp.updateWitness(0, ScriptWitness(Seq(sig))) + // We still need to provide the tapscript tree because it is used to tweak the private key. + val sig = Transaction.signInputTaprootKeyPath(privs(0), tmp, 0, Seq(fundingTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) + tmp.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) } // see: https://mempool.space/signet/tx/de3e4dcf07e68c7b237269eee75b926b9d147869f6317031b0550dcbf509ff5b @@ -253,7 +248,7 @@ class TaprootSpec extends FunSuite { // see https://mempool.space/signet/tx/193962bdc619a1c6f28e3989603a229055b544ee9e12c5ca8cc0a694babd8506 val fundingTx1 = Transaction.read("020000000001032c94e663cbee0edbdb4375bb2e79be60f8ecfa4e936a14e9a054b1c8923928570000000000feffffff308788df38f369e33bcd70765c171a9796d910b02525a550bfe4d2a2cf8a710c0100000000feffffff94dc10cd523655b0323e90428d720b378b91de312e56908325df6878c530d30d0000000000feffffff0200e1f50500000000225120f1d062d20433c023ba934aec75fa78cf8e2f840181b88c301430524aad93d0fb8b4f174e020000001600140e361914cb87862fb6ea24193331d6591b59859002463043021f5dcc64a2fef28bdd2b88b5d10851079cc98663a1284d0569bdde5afc558fb202205c2bcdcf1dae62b2c32e8cf6ac6cb2534b70b1889be893da170564a8c4d40f2001210270b71142cd209ddd686ef013adaeb12b641fde95d589a5a607ee0b6c95cc086202473044022034121d55d61376aee90f6b975522b6bad85491448d527b83f6dacbdddcd9548202201a0a9405542ae06239fabdc01069fe2518ee7340ed400d4db2d92604f9d454d601210319b3ad1b37d95ab41034cd810799149501e62ab6d009a6a4eca6034f78ca725b024730440220487663d7740eaa5370673f4807497970feb2d69c83cae281d89fef8aa616259a02200a21dc493e455c2980bc245224eb67aba576f732f77af0fd555a5f44fa205e4d0121023a34e31279a234431b349fd229790038c95c837a8139862df9cbb1226d63c4003eb10100") - assert(fundingTx1.txOut.head.publicKeyScript == script) + assert(fundingTx1.txOut.head.publicKeyScript == Script.write(script)) // spending with script #1 val tx1 = { @@ -263,15 +258,9 @@ class TaprootSpec extends FunSuite { txOut = Seq(TxOut(fundingTx1.txOut.head.amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - // to re-compute the merkle root we need to provide leaves #2 and #3 - val controlBlock = ByteVector.view(Array((fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte) ++ - internalPubkey.pub.value.toByteArray ++ - ScriptTree.hash(leaves(1)).toByteArray ++ - ScriptTree.hash(leaves(2)).toByteArray) - - val hash = hashForSigningSchnorr(tmp, 0, Seq(fundingTx.txOut.head), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(None, Some(ScriptTree.hash(leaves.head)))) - val sig = Crypto.signSchnorr(hash, privs.head, fr.acinq.bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE) - tmp.updateWitness(0, ScriptWitness(Seq(sig, Script.write(scripts.head), controlBlock))) + val sig = Transaction.signInputTaprootScriptPath(privs(0), tmp, 0, Seq(fundingTx.txOut(0)), SigHash.SIGHASH_DEFAULT, leaves(0).hash()) + val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves(0), ScriptWitness(Seq(sig)), scriptTree) + tmp.updateWitness(0, witness) } // see: https://mempool.space/signet/tx/5586515f9ed7fce8b7e8be97a8681c298a94166ff95e15edd94226edec50d9ea @@ -280,7 +269,7 @@ class TaprootSpec extends FunSuite { // see https://mempool.space/signet/tx/b4dfa342b434709e1b4fd46a2caf7661a195267445ba4402bb2364b174edc5a6 val fundingTx2 = Transaction.read("02000000000101c1952516d2f512e8ec29ffe576fcb13903987434ce22479f2e18b5060f0184c20100000000feffffff0200e1f50500000000225120f1d062d20433c023ba934aec75fa78cf8e2f840181b88c301430524aad93d0fb28b1b61100000000160014665ea2d5f8f03b7edc82472baed5ba28dcd22a9f024730440220014381ea4fc0e96733231b84bf9d24ee6d197147c2d2842c896530103c9c23310220384d174f4578767f2117c558671e592ea497f0680cedbacc73dc3f4c316f6b73012102d2212f3a1ef1a797be1fbe8ac784eb81158957339cab89e32faa6f73cc9bf6713fb10100") - assert(fundingTx2.txOut.head.publicKeyScript == script) + assert(fundingTx2.txOut.head.publicKeyScript == Script.write(script)) // spending with script #2 // it's basically the same as for key #1 @@ -291,14 +280,9 @@ class TaprootSpec extends FunSuite { txOut = Seq(TxOut(fundingTx2.txOut.head.amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - // to re-compute the merkle root we need to provide leaves #1 and #3 - val controlBlock = ByteVector.view(Array((fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte) ++ - internalPubkey.pub.value.toByteArray ++ - ScriptTree.hash(leaves.head).toByteArray ++ - ScriptTree.hash(leaves(2)).toByteArray) - val hash = hashForSigningSchnorr(tmp, 0, Seq(fundingTx2.txOut.head), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(None, Some(ScriptTree.hash(leaves(1))))) - val sig = Crypto.signSchnorr(hash, privs(1), fr.acinq.bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE) // signature for script spend of leaf #2 - tmp.updateWitness(0, ScriptWitness(Seq(sig, Script.write(scripts(1)), controlBlock))) + val sig = Transaction.signInputTaprootScriptPath(privs(1), tmp, 0, Seq(fundingTx2.txOut(0)), SigHash.SIGHASH_DEFAULT, leaves(1).hash()) + val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves(1), ScriptWitness(Seq(sig)), scriptTree) + tmp.updateWitness(0, witness) } // see: https://mempool.space/signet/tx/5586515f9ed7fce8b7e8be97a8681c298a94166ff95e15edd94226edec50d9ea @@ -307,7 +291,7 @@ class TaprootSpec extends FunSuite { // see https://mempool.space/signet/tx/97196e1dc3ee089955d2a738143a66a34166d0c7f0a85d8ad4ba2c972dc0555c val fundingTx3 = Transaction.read("020000000001025bff09f5cb0d55b0317031f66978149d6b925be7ee6972237b8ce607cf4d3ede0000000000feffffffead950eced2642d9ed155ef96f16948a291c68a897bee8b7e8fcd79e5f5186550000000000feffffff0214b9f50500000000160014faf51bb67e3e35a93aa549cf2c8d24763d8162ce00e1f50500000000225120f1d062d20433c023ba934aec75fa78cf8e2f840181b88c301430524aad93d0fb0247304402201989eb9d1f4d976a9f0bf512e7f1fa784c45eee369a6c13511162a463c89935002201a1d41e53c56600137a851d0c26daaffd6aa30197fbf9221daf6cbca458fb40f012102238ee9a8b833398e3421c809e7ac75089e4e738841577273fe87d3cd14a22cf202473044022035e887ced3bb03f54cce39e4cdecf93b787765c51de2545a16c97fec67d3085b02200bd15d5497d1a9be37ad29142673ef2cdc0cee69f6a9cf5643c376a4b4f81489012102238ee9a8b833398e3421c809e7ac75089e4e738841577273fe87d3cd14a22cf290b10100") - assert(fundingTx3.txOut(1).publicKeyScript == script) + assert(fundingTx3.txOut(1).publicKeyScript == Script.write(script)) // spending with script #3 val tx3 = { @@ -317,17 +301,14 @@ class TaprootSpec extends FunSuite { txOut = Seq(TxOut(fundingTx3.txOut.head.amount - Satoshi(5000), addressToPublicKeyScript(blockchain, "tb1qxy9hhxkw7gt76qrm4yzw4j06gkk4evryh8ayp7").toOption.get)), lockTime = 0 ) - // to re-compute the merkle root we need to provide branch(#1, #2) - val controlBlock = ByteVector.view(Array((fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte) ++ - internalPubkey.pub.value.toByteArray ++ - ScriptTree.hash(new ScriptTree.Branch(leaves.head, leaves(1))).toByteArray) - val hash = hashForSigningSchnorr(tmp, 0, Seq(fundingTx3.txOut(1)), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(None, Some(ScriptTree.hash(leaves(2))))) - val sig = Crypto.signSchnorr(hash, privs(2), fr.acinq.bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE) // signature for script spend of leaf #3 - tmp.updateWitness(0, ScriptWitness(Seq(sig, Script.write(scripts(2)), controlBlock))) + val sig = Transaction.signInputTaprootScriptPath(privs(2), tmp, 0, Seq(fundingTx3.txOut(1)), SigHash.SIGHASH_DEFAULT, leaves(2).hash()) + val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves(2), ScriptWitness(Seq(sig)), scriptTree) + tmp.updateWitness(0, witness) } // see: https://mempool.space/signet/tx/2eb421e044de0535aa3d14a5a4c325ba8b5181440bbd911b5b43718b686b09a8 assert(tx3.toString() == "020000000001015c55c02d972cbad48a5da8f0c7d06641a3663a1438a7d2559908eec31d6e19970100000000ffffffff018ca5f50500000000160014310b7b9acef217ed007ba904eac9fa45ad5cb0640340c10da2636457db468385345303e984ee949d0815745f5dcba67cde603ef02738b6f26f6c44beef0a93d9fcbb82571d215ca2cf04a1894ce01d2eaf7b6068260a2220a4fbd2c1822592c0ae8afa0e63a0d4c56a571179e93fd61615627f419fd0be9aac41c01b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f35b9c8be6dc0c33d6ce3cc9d3ba04c509b3f5b0139254f67d3184a5a238901f400000000") Transaction.correctlySpends(tx3, fundingTx3 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + }