Skip to content

Commit 76d89a4

Browse files
committed
Add helpers to spend taproot key path or script paths
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.
1 parent a0c5bd5 commit 76d89a4

File tree

7 files changed

+185
-107
lines changed

7 files changed

+185
-107
lines changed

src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public object Crypto {
164164
public object NoTweak : SchnorrTweak()
165165
}
166166

167-
public sealed class TaprootTweak: SchnorrTweak() {
167+
public sealed class TaprootTweak : SchnorrTweak() {
168168
/**
169169
* private key is tweaked with H_TapTweak(public key) (this is used for key path spending when no scripts are present)
170170
*/
@@ -173,8 +173,11 @@ public object Crypto {
173173
/**
174174
* private key is tweaked with H_TapTweak(public key || merkle_root) (this is used for key path spending, with specific Merkle root of the script tree).
175175
*/
176-
public data class ScriptTweak(val merkleRoot: ByteVector32) : TaprootTweak()
176+
public data class ScriptTweak(val merkleRoot: ByteVector32) : TaprootTweak() {
177+
public constructor(scriptTree: ScriptTree) : this(scriptTree.hash())
178+
}
177179
}
180+
178181
/**
179182
* @param data data to sign (32 bytes)
180183
* @param privateKey private key
@@ -184,7 +187,7 @@ public object Crypto {
184187
*/
185188
@JvmStatic
186189
public fun signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak: SchnorrTweak, auxrand32: ByteVector32? = null): ByteVector64 {
187-
val priv = when(schnorrTweak) {
190+
val priv = when (schnorrTweak) {
188191
SchnorrTweak.NoTweak -> privateKey
189192
is TaprootTweak.NoScriptTweak -> privateKey.tweak(privateKey.xOnlyPublicKey().tweak(schnorrTweak))
190193
is TaprootTweak.ScriptTweak -> privateKey.tweak(privateKey.xOnlyPublicKey().tweak(schnorrTweak))
@@ -194,6 +197,24 @@ public object Crypto {
194197
return sig
195198
}
196199

200+
/** Produce a signature that will be included in the witness of a taproot key path spend. */
201+
@JvmStatic
202+
public fun signTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List<TxOut>, sighashType: Int, scriptTree: ScriptTree?, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 {
203+
val data = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, inputs, sighashType, annex)
204+
val tweak = when (scriptTree) {
205+
null -> TaprootTweak.NoScriptTweak
206+
else -> TaprootTweak.ScriptTweak(scriptTree.hash())
207+
}
208+
return signSchnorr(data, privateKey, tweak, auxrand32)
209+
}
210+
211+
/** Produce a signature that will be included in the witness of a taproot script path spend. */
212+
@JvmStatic
213+
public fun signTaprootScriptPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List<TxOut>, sighashType: Int, tapleaf: ByteVector32, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 {
214+
val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, inputs, sighashType, tapleaf, annex)
215+
return signSchnorr(data, privateKey, SchnorrTweak.NoTweak, auxrand32)
216+
}
217+
197218
@JvmStatic
198219
public fun verifySignatureSchnorr(data: ByteVector32, signature: ByteVector, publicKey: XonlyPublicKey): Boolean {
199220
return Secp256k1.verifySchnorr(signature.toByteArray(), data.toByteArray(), publicKey.value.toByteArray())

src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,20 @@ public object Script {
386386
@JvmStatic
387387
public fun isPay2wsh(script: ByteArray): Boolean = isPay2wsh(parse(script))
388388

389+
@JvmStatic
390+
public fun isPay2tr(script: List<ScriptElt>): Boolean {
391+
return when {
392+
script.size == 2 && script[0] == OP_1 && script[1].isPush(32) -> true
393+
else -> false
394+
}
395+
}
396+
397+
@JvmStatic
398+
public fun isPay2tr(script: ByteArray): Boolean = isPay2tr(parse(script))
399+
400+
@JvmStatic
401+
public fun isPay2tr(script: ByteVector): Boolean = isPay2tr(script.toByteArray())
402+
389403
/**
390404
* @param pubKeyHash public key hash
391405
* @return a pay-to-public-key-hash script
@@ -404,47 +418,41 @@ public object Script {
404418
public fun pay2pkh(pubKey: PublicKey): List<ScriptElt> = pay2pkh(pubKey.hash160())
405419

406420
/**
407-
*
408421
* @param script bitcoin script
409422
* @return a pay-to-script script
410423
*/
411424
@JvmStatic
412425
public fun pay2sh(script: List<ScriptElt>): List<ScriptElt> = pay2sh(write(script))
413426

414427
/**
415-
*
416428
* @param script bitcoin script
417429
* @return a pay-to-script script
418430
*/
419431
@JvmStatic
420432
public fun pay2sh(script: ByteArray): List<ScriptElt> = listOf(OP_HASH160, OP_PUSHDATA(Crypto.hash160(script)), OP_EQUAL)
421433

422434
/**
423-
*
424435
* @param script bitcoin script
425436
* @return a pay-to-witness-script script
426437
*/
427438
@JvmStatic
428439
public fun pay2wsh(script: List<ScriptElt>): List<ScriptElt> = pay2wsh(write(script))
429440

430441
/**
431-
*
432442
* @param script bitcoin script
433443
* @return a pay-to-witness-script script
434444
*/
435445
@JvmStatic
436446
public fun pay2wsh(script: ByteArray): List<ScriptElt> = listOf(OP_0, OP_PUSHDATA(Crypto.sha256(script)))
437447

438448
/**
439-
*
440449
* @param script bitcoin script
441450
* @return a pay-to-witness-script script
442451
*/
443452
@JvmStatic
444453
public fun pay2wsh(script: ByteVector): List<ScriptElt> = pay2wsh(script.toByteArray())
445454

446455
/**
447-
*
448456
* @param pubKeyHash public key hash
449457
* @return a pay-to-witness-public-key-hash script
450458
*/
@@ -455,20 +463,12 @@ public object Script {
455463
}
456464

457465
/**
458-
*
459466
* @param pubKey public key
460467
* @return a pay-to-witness-public-key-hash script
461468
*/
462469
@JvmStatic
463470
public fun pay2wpkh(pubKey: PublicKey): List<ScriptElt> = pay2wpkh(pubKey.hash160())
464471

465-
/**
466-
* @param pubkey x-only public key
467-
* @return a pay-to-taproot script
468-
*/
469-
@JvmStatic
470-
public fun pay2tr(pubkey: XonlyPublicKey): List<ScriptElt> = listOf(OP_1, OP_PUSHDATA(pubkey.value))
471-
472472
/**
473473
* @param pubKey public key
474474
* @param sig signature matching the public key
@@ -477,6 +477,47 @@ public object Script {
477477
@JvmStatic
478478
public fun witnessPay2wpkh(pubKey: PublicKey, sig: ByteVector): ScriptWitness = ScriptWitness(listOf(sig, pubKey.value))
479479

480+
/**
481+
* @param outputKey public key exposed by the taproot script (tweaked based on the tapscripts).
482+
* @return a pay-to-taproot script.
483+
*/
484+
@JvmStatic
485+
public fun pay2tr(outputKey: XonlyPublicKey): List<ScriptElt> = listOf(OP_1, OP_PUSHDATA(outputKey.value))
486+
487+
/**
488+
* @param internalKey internal public key that will be tweaked with the [scripts] provided.
489+
* @param scripts optional spending scripts that can be used instead of key-path spending.
490+
* @return the script and the tweak that must be applied to the private key for [internalKey] when signing.
491+
*/
492+
@JvmStatic
493+
public fun pay2tr(internalKey: XonlyPublicKey, scripts: ScriptTree?): List<ScriptElt> {
494+
val tweak = when (scripts) {
495+
null -> Crypto.TaprootTweak.NoScriptTweak
496+
else -> Crypto.TaprootTweak.ScriptTweak(scripts.hash())
497+
}
498+
val (publicKey, _) = internalKey.outputKey(tweak)
499+
return pay2tr(publicKey)
500+
}
501+
502+
/** NB: callers must ensure that they use the correct [Crypto.TaprootTweak] when generating their signature. */
503+
@JvmStatic
504+
public fun witnessKeyPathPay2tr(sig: ByteVector64, sighash: Int = SigHash.SIGHASH_DEFAULT): ScriptWitness = when (sighash) {
505+
SigHash.SIGHASH_DEFAULT -> ScriptWitness(listOf(sig))
506+
else -> ScriptWitness(listOf(sig.concat(sighash.toByte())))
507+
}
508+
509+
/**
510+
* @param internalKey taproot internal public key.
511+
* @param script script that is spent (must exist in the [scriptTree]).
512+
* @param witness witness for the spent [script].
513+
* @param scriptTree tapscript tree.
514+
*/
515+
@JvmStatic
516+
public fun witnessScriptPathPay2tr(internalKey: XonlyPublicKey, script: ScriptTree.Leaf, witness: ScriptWitness, scriptTree: ScriptTree): ScriptWitness {
517+
val controlBlock = ControlBlock.build(internalKey, scriptTree, script)
518+
return ScriptWitness(witness.stack + script.script + controlBlock)
519+
}
520+
480521
public fun removeSignature(script: List<ScriptElt>, signature: ByteVector): List<ScriptElt> {
481522
val toRemove = OP_PUSHDATA(signature)
482523
return script.filterNot { it == toRemove }
@@ -622,7 +663,7 @@ public object Script {
622663
*/
623664
@JvmStatic
624665
public fun build(internalPubKey: XonlyPublicKey, scriptTree: ScriptTree, spendingScript: ScriptTree.Leaf): ByteVector {
625-
val (_, parity) = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(scriptTree.hash()))
666+
val (_, parity) = internalPubKey.outputKey(scriptTree)
626667
val controlByte = (spendingScript.leafVersion + (if (parity) 1 else 0)).toByte()
627668
// NB: the spending script is included in a separate witness element, so we remove it from the control block.
628669
val merkleProof = scriptTree.merkleProof(spendingScript.id).tail()
@@ -689,7 +730,16 @@ public object Script {
689730
pubKey.size == 32 && sigBytes.isEmpty() -> false
690731
pubKey.size == 32 -> {
691732
val sighashType = sigHashType(sigBytes)
692-
val hash = Transaction.hashForSigningSchnorr(context.tx, context.inputIndex, context.prevouts, sighashType, signatureVersion, this.context.executionData)
733+
val hash = Transaction.hashForSigningSchnorr(
734+
context.tx,
735+
context.inputIndex,
736+
context.prevouts,
737+
sighashType,
738+
signatureVersion,
739+
context.executionData.tapleafHash,
740+
context.executionData.annex,
741+
context.executionData.codeSeparatorPos
742+
)
693743
val result = Secp256k1.verifySchnorr(sigBytes.take(64).toByteArray(), hash.toByteArray(), pubKey)
694744
require(result) { "Invalid Schnorr signature" }
695745
result
@@ -1364,7 +1414,16 @@ public object Script {
13641414
val sig = stack.first()
13651415
val pub = XonlyPublicKey(program.byteVector32())
13661416
val hashType = sigHashType(sig)
1367-
val hash = Transaction.hashForSigningSchnorr(context.tx, context.inputIndex, context.prevouts, hashType, SigVersion.SIGVERSION_TAPROOT, context.executionData)
1417+
val hash = Transaction.hashForSigningSchnorr(
1418+
context.tx,
1419+
context.inputIndex,
1420+
context.prevouts,
1421+
hashType,
1422+
SigVersion.SIGVERSION_TAPROOT,
1423+
context.executionData.tapleafHash,
1424+
context.executionData.annex,
1425+
context.executionData.codeSeparatorPos
1426+
)
13681427
require(Secp256k1.verifySchnorr(sig.take(64).toByteArray(), hash.toByteArray(), pub.value.toByteArray())) { " invalid Schnorr signature " }
13691428
return
13701429
} else {

src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,8 @@ public data class Transaction(
731731
* @param inputs UTXOs spent by this transaction
732732
* @param sighashType signature hash type
733733
* @param sigVersion signature version
734-
* @param executionData execution context of a transaction script
734+
* @param tapleaf when spending a tapscript, the hash of the corresponding script leaf must be provided
735+
* @param annex (optional) taproot annex
735736
*/
736737
@JvmStatic
737738
public fun hashForSigningSchnorr(
@@ -740,7 +741,9 @@ public data class Transaction(
740741
inputs: List<TxOut>,
741742
sighashType: Int,
742743
sigVersion: Int,
743-
executionData: Script.ExecutionData = Script.ExecutionData.empty
744+
tapleaf: ByteVector32? = null,
745+
annex: ByteVector? = null,
746+
codeSeparatorPos: Long? = null,
744747
): ByteVector32 {
745748
val out = ByteArrayOutput()
746749
out.write(0)
@@ -753,7 +756,7 @@ public data class Transaction(
753756
SigVersion.SIGVERSION_TAPSCRIPT -> Pair(1, 0)
754757
else -> Pair(0, 0)
755758
}
756-
val spendType = 2 * extFlag + (if (executionData.annex != null) 1 else 0)
759+
val spendType = 2 * extFlag + (if (annex != null) 1 else 0)
757760
out.write(spendType)
758761
val inputType = sighashType and SigHash.SIGHASH_INPUT_MASK
759762
if (inputType == SigHash.SIGHASH_ANYONECANPAY) {
@@ -763,9 +766,9 @@ public data class Transaction(
763766
} else {
764767
writeUInt32(inputIndex.toUInt(), out)
765768
}
766-
if (executionData.annex != null) {
769+
if (annex != null) {
767770
val buffer = ByteArrayOutput()
768-
writeScript(executionData.annex, buffer)
771+
writeScript(annex, buffer)
769772
val annexHash = Crypto.sha256(buffer.toByteArray())
770773
out.write(annexHash)
771774
}
@@ -774,15 +777,27 @@ public data class Transaction(
774777
out.write(Crypto.sha256(TxOut.write(tx.txOut[inputIndex])))
775778
}
776779
if (sigVersion == SigVersion.SIGVERSION_TAPSCRIPT) {
777-
require(executionData.tapleafHash != null) { "tapleaf hash is missing" }
778-
out.write(executionData.tapleafHash.toByteArray())
780+
require(tapleaf != null) { "tapleaf hash is missing" }
781+
out.write(tapleaf.toByteArray())
779782
out.write(keyVersion)
780-
writeUInt32(executionData.codeSeparatorPos.toUInt(), out)
783+
writeUInt32((codeSeparatorPos ?: 0xFFFFFFFFL).toUInt(), out)
781784
}
782785
val preimage = out.toByteArray()
783786
return Crypto.taggedHash(preimage, "TapSighash")
784787
}
785788

789+
/** Use this function when spending a taproot key path. */
790+
@JvmStatic
791+
public fun hashForSigningTaprootKeyPath(tx: Transaction, inputIndex: Int, inputs: List<TxOut>, sighashType: Int, annex: ByteVector? = null): ByteVector32 {
792+
return hashForSigningSchnorr(tx, inputIndex, inputs, sighashType, SigVersion.SIGVERSION_TAPROOT, annex = annex)
793+
}
794+
795+
/** Use this function when spending a taproot script path. */
796+
@JvmStatic
797+
public fun hashForSigningTaprootScriptPath(tx: Transaction, inputIndex: Int, inputs: List<TxOut>, sighashType: Int, tapleaf: ByteVector32, annex: ByteVector? = null): ByteVector32 {
798+
return hashForSigningSchnorr(tx, inputIndex, inputs, sighashType, SigVersion.SIGVERSION_TAPSCRIPT, tapleaf, annex)
799+
}
800+
786801
@JvmStatic
787802
public fun correctlySpends(tx: Transaction, previousOutputs: Map<OutPoint, TxOut>, scriptFlags: Int) {
788803
val prevouts = tx.txIn.map { previousOutputs[it.outPoint]!! }

src/commonMain/kotlin/fr/acinq/bitcoin/XonlyPublicKey.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@ public data class XonlyPublicKey(@JvmField val value: ByteVector32) {
3434
}
3535

3636
/**
37-
* "tweaks" this key with an optional merkle root
37+
* Tweak this key with an optional merkle root.
38+
*
3839
* @param tapTweak taproot tweak
3940
* @return an (x-only pubkey, parity) pair
4041
*/
4142
public fun outputKey(tapTweak: Crypto.TaprootTweak): Pair<XonlyPublicKey, Boolean> = this + PrivateKey(tweak(tapTweak)).publicKey()
4243

44+
/** Tweak this key with the merkle root of the given script tree. */
45+
public fun outputKey(scriptTree: ScriptTree): Pair<XonlyPublicKey, Boolean> = outputKey(Crypto.TaprootTweak.ScriptTweak(scriptTree))
46+
4347
/**
4448
* add a public key to this x-only key
4549
* @param that public key

0 commit comments

Comments
 (0)