Skip to content

Commit

Permalink
Add helper method to build the control block for taproot transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Nov 28, 2023
1 parent e4f79f3 commit 92223a9
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 14 deletions.
45 changes: 45 additions & 0 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,30 @@ public object Script {
require(head.data.size() == head.code)
out.write(head.data.size())
}

head.code == 0x4c -> {
require(head.data.size() <= 0xff)
BtcSerializer.writeUInt8(0x4Cu, out)
BtcSerializer.writeUInt8(head.data.size().toUByte(), out)
}

head.code == 0x4d -> {
require(head.data.size() <= 0xffff)
BtcSerializer.writeUInt8(0x4Du, out)
BtcSerializer.writeUInt16(head.data.size().toUShort(), out)
}

head.code == 0x4e -> {
require(head.data.size() <= 0xffffffff)
BtcSerializer.writeUInt8(0x4Eu, out)
BtcSerializer.writeUInt32(head.data.size().toUInt(), out)
}

else -> error("invalid OP_PUSHADATA opcode ${head.code}")
}
out.write(head.data.toByteArray())
}

else -> {
out.write(head.code)
}
Expand Down Expand Up @@ -595,6 +600,34 @@ public object Script {
}
}

public object ControlBlock {
/**
* build a control block to add to the witness of a taproot transaction when spending with the script path.
* It includes information to re-compute the merkle root from the script you are using.
*
* For example, suppose you have the following script tree:
* root
* / \
* / \ #3
* #1 #2
*
* To recompute its merkle root you need to provide either:
* - if you're spending with script #1: leaves #2 and #3
* - if you're spending with script #2: leaves #1 and #3
* - if you're spending with script #3: branch(#1, #2)
*
* @param internalPubKey internal public key
* @param merkleRoot tapscript merkle root
* @param leaves list of partial script trees than are required to re-build the merkle root
*/
@JvmStatic
public fun build(internalPubKey: XonlyPublicKey, merkleRoot: ByteVector32, leaves: List<ScriptTree<ScriptLeaf>> = listOf()): ByteArray {
val parity = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)).second
val initialBlock = byteArrayOf((TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray()
return leaves.fold(initialBlock) { block, tree -> block + ScriptTree.hash(tree).toByteArray() }
}
}

public class Runner(
public val context: Context,
public val scriptFlag: Int = ScriptFlags.MANDATORY_SCRIPT_VERIFY_FLAGS,
Expand Down Expand Up @@ -659,6 +692,7 @@ public object Script {
require(result) { "Invalid Schnorr signature" }
result
}

else -> {
require((scriptFlag and ScriptFlags.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE) == 0) { "invalid pubkey type" }
sigBytes.isNotEmpty()
Expand Down Expand Up @@ -777,6 +811,7 @@ public object Script {
op == OP_IF && conditions.any { !it } -> {
conditions.add(0, false)
}

op == OP_IF && stack.isEmpty() -> throw RuntimeException("Invalid OP_IF construction")
op == OP_IF -> {
val stackhead = stack.removeFirst()
Expand All @@ -793,6 +828,7 @@ public object Script {
op == OP_NOTIF && conditions.any { !it } -> {
conditions.add(0, true)
}

op == OP_NOTIF && stack.isEmpty() -> throw RuntimeException("Invalid OP_NOTIF construction")
op == OP_NOTIF -> {
val stackhead = stack.removeFirst()
Expand Down Expand Up @@ -830,6 +866,7 @@ public object Script {
op == OP_1ADD -> {
stack[0] = encodeNumber(decodeNumber(stack.first()) + 1)
}

op == OP_1SUB && stack.isEmpty() -> throw RuntimeException("cannot run OP_1SUB on an empty stack")
op == OP_1SUB -> {
stack[0] = encodeNumber(decodeNumber(stack.first()) - 1)
Expand Down Expand Up @@ -884,6 +921,7 @@ public object Script {
if (locktime < 0) throw RuntimeException("CLTV lock time cannot be negative")
if (!checkLockTime(locktime, context.tx, context.inputIndex)) throw RuntimeException("unsatisfied CLTV lock time")
}

op == OP_CHECKLOCKTIMEVERIFY && ((scriptFlag and ScriptFlags.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS) != 0) -> throw RuntimeException("use of upgradable NOP is discouraged")
op == OP_CHECKLOCKTIMEVERIFY -> {}

Expand All @@ -906,6 +944,7 @@ public object Script {
if (!checkSequence(sequence, context.tx, context.inputIndex)) throw RuntimeException("unsatisfied CSV lock time")
}
}

op == OP_CHECKSEQUENCEVERIFY && ((scriptFlag and ScriptFlags.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS) != 0) -> throw RuntimeException("use of upgradable NOP is discouraged")
op == OP_CHECKSEQUENCEVERIFY -> {}

Expand Down Expand Up @@ -1299,13 +1338,15 @@ public object Script {
val finalStack = run(listOf(OP_DUP, OP_HASH160, OP_PUSHDATA(program), OP_EQUALVERIFY, OP_CHECKSIG), witness.stack.reversed(), SigVersion.SIGVERSION_WITNESS_V0)
checkFinalStack(finalStack)
}

witnessVersion == 0L && program.size == WITNESS_V0_SCRIPTHASH_SIZE -> {
// P2WPSH, program is the hash of the script, and witness is the stack + the script
val check = Crypto.sha256(witness.stack.last())
require(check.contentEquals(program)) { "witness program mismatch" }
val finalStack = run(witness.stack.last(), witness.stack.dropLast(1).reversed(), SigVersion.SIGVERSION_WITNESS_V0)
checkFinalStack(finalStack)
}

witnessVersion == 0L -> throw IllegalArgumentException("Invalid witness program length: ${program.size}")
witnessVersion == 1L && program.size == WITNESS_V1_TAPROOT_SIZE && !isP2sh -> {
// BIP341 Taproot: 32-byte non-P2SH witness v1 program (which encodes a P2C-tweaked pubkey)
Expand Down Expand Up @@ -1369,6 +1410,7 @@ public object Script {
}
}
}

(scriptFlag and ScriptFlags.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) != 0 -> throw IllegalArgumentException("Witness version $witnessVersion reserved for soft-fork upgrades")
else -> {
// Higher version witness scripts return true for future softfork compatibility
Expand Down Expand Up @@ -1404,6 +1446,7 @@ public object Script {
if ((scriptFlag and ScriptFlags.SCRIPT_VERIFY_P2SH) == 0) throw RuntimeException("illegal script flag")
stack.size == 1
}

else -> true
}

Expand Down Expand Up @@ -1438,6 +1481,7 @@ public object Script {
verifyWitnessProgram(witness, witnessVersion.toLong(), program.data.toByteArray(), isP2sh = false)
stack0.take(1)
}

else -> stack0
}
} else stack0
Expand Down Expand Up @@ -1465,6 +1509,7 @@ public object Script {
verifyWitnessProgram(witness, witnessVersion.toLong(), (program[1] as OP_PUSHDATA).data.toByteArray(), isP2sh = true)
stackp2sh.take(1)
}

else -> stackp2sh
}
} else stackp2sh
Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/fr/acinq/bitcoin/ScriptTree.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import kotlin.jvm.JvmStatic
* @param leafVersion tapscript version
*/
public data class ScriptLeaf(val id: Int, val script: ByteVector, val leafVersion: Int) {
public constructor(id: Int, script: List<ScriptElt>, leafVersion: Int) : this(id, Script.write(script).byteVector(), leafVersion)
/**
* tapleaf hash of this leaf
*/
Expand Down
78 changes: 78 additions & 0 deletions src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fr.acinq.bitcoin.musig2.SessionCtx
import fr.acinq.bitcoin.reference.TransactionTestsCommon
import fr.acinq.secp256k1.Hex
import kotlinx.serialization.json.*
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
Expand Down Expand Up @@ -323,4 +324,81 @@ class Musig2TestsCommon {
val signedSpendingTx = spendingTx.updateWitness(0, ScriptWitness(listOf(commonSig)))
Transaction.correctlySpends(signedSpendingTx, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

@Test
fun `swap-in-potentiam example with musig2 and taproot`() {
val userPrivateKey = PrivateKey(ByteArray(32) { 1 })
val serverPrivateKey = PrivateKey(ByteArray(32) { 2 })
val userRefundPrivateKey = PrivateKey(ByteArray(32) { 3 })
val refundDelay = 25920

val random = Random

// 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 = listOf(OP_PUSHDATA(userRefundPrivateKey.publicKey().xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY)
val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, redeemScript, Script.TAPROOT_LEAF_TAPSCRIPT))
val merkleRoot = ScriptTree.hash(scriptTree)

// 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 internalPubKey = Musig2.keyAgg(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())).Q.xOnly()

// it is tweaked with the script's merkle root to get the pubkey that will be exposed
val (commonPubKey, _) = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot))
val pubkeyScript: List<ScriptElt> = Script.pay2tr(commonPubKey)

val swapInTx = Transaction(
version = 2,
txIn = listOf(),
txOut = listOf(TxOut(Satoshi(10000), pubkeyScript)),
lockTime = 0
)

// The transaction can be spent if the user and the server produce a signature.
run {
val tx = Transaction(
version = 2,
txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)),
txOut = listOf(TxOut(Satoshi(10000), Script.pay2wpkh(userPrivateKey.publicKey()))),
lockTime = 0
)
// this is the beginning of an interactive musig2 signing session. if user and server are disconnected before they have exchanged partial
// signatures they will have to start again with fresh nonces
val userNonce = SecretNonce.generate(userPrivateKey, userPrivateKey.publicKey(), internalPubKey, null, null, random.nextBytes(32).byteVector32())
val serverNonce = SecretNonce.generate(serverPrivateKey, serverPrivateKey.publicKey(), internalPubKey, null, null, random.nextBytes(32).byteVector32())

val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT)
val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce.publicNonce()))
val ctx = SessionCtx(
commonNonce,
listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey()),
listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)),
txHash
)

val userSig = ctx.sign(userNonce, userPrivateKey)
val serverSig = ctx.sign(serverNonce, serverPrivateKey)
val commonSig = ctx.partialSigAgg(listOf(userSig, serverSig))
val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig)))
Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

// Or it can be spent with only the user's signature, after a delay.
run {
val executionData = Script.ExecutionData(annex = null, tapleafHash = merkleRoot)
val controlBlock = Script.ControlBlock.build(internalPubKey, merkleRoot)

val tx = Transaction(
version = 2,
txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())),
txOut = listOf(TxOut(Satoshi(10000), Script.pay2wpkh(userPrivateKey.publicKey()))),
lockTime = 0
)
val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, executionData)

val sig = Crypto.signSchnorr(txHash, userRefundPrivateKey, Crypto.SchnorrTweak.NoTweak)
val signedTx = tx.updateWitness(0, ScriptWitness.empty.push(sig).push(redeemScript).push(controlBlock))
Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
}
}
20 changes: 6 additions & 14 deletions src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class TaprootTestsCommon {
val merkleRoot = ScriptTree.hash(scriptTree)
// 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.fromHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"))
val (tweakedKey, parity) = internalPubkey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot))
val (tweakedKey, _) = internalPubkey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot))

// funding tx sends to our tapscript
val fundingTx = Transaction(version = 2, txIn = listOf(), txOut = listOf(TxOut(Satoshi(1000000), listOf(OP_1, OP_PUSHDATA(tweakedKey)))), lockTime = 0)
Expand All @@ -197,7 +197,7 @@ class TaprootTestsCommon {
val sigs = privs.map { Crypto.signSchnorr(hash, it, Crypto.SchnorrTweak.NoTweak) }

// control is the same for everyone since there are no specific merkle hashes to provide
val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubkey.value.toByteArray()
val controlBlock = Script.ControlBlock.build(internalPubkey, merkleRoot)

// one signature is not enough
val tx = tmp.updateWitness(0, ScriptWitness(listOf(sigs[0], sigs[0], sigs[0], Script.write(script).byteVector(), controlBlock.byteVector())))
Expand Down Expand Up @@ -241,7 +241,7 @@ class TaprootTestsCommon {

// we use key #1 as our internal key
val internalPubkey = XonlyPublicKey(privs[0].publicKey())
val (tweakedKey, parity) = internalPubkey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot))
val (tweakedKey, _) = internalPubkey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot))

// this is the tapscript we send funds to
val script = Script.write(listOf(OP_1, OP_PUSHDATA(tweakedKey.value))).byteVector()
Expand Down Expand Up @@ -287,10 +287,7 @@ class TaprootTestsCommon {
lockTime = 0
)
// to re-compute the merkle root we need to provide leaves #2 and #3
val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) +
internalPubkey.value.toByteArray() +
ScriptTree.hash(leaves[1]).toByteArray() +
ScriptTree.hash(leaves[2]).toByteArray()
val controlBlock = Script.ControlBlock.build(internalPubkey, merkleRoot, listOf(leaves[1], leaves[2]))
val hash = hashForSigningSchnorr(tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(null, ScriptTree.hash(leaves[0])))
val sig = Crypto.signSchnorr(hash, privs[0], Crypto.SchnorrTweak.NoTweak)
tmp.updateWitness(0, ScriptWitness(listOf(sig, Script.write(scripts[0]).byteVector(), controlBlock.byteVector())))
Expand All @@ -314,10 +311,7 @@ class TaprootTestsCommon {
lockTime = 0
)
// to re-compute the merkle root we need to provide leaves #1 and #3
val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) +
internalPubkey.value.toByteArray() +
ScriptTree.hash(leaves[0]).toByteArray() +
ScriptTree.hash(leaves[2]).toByteArray()
val controlBlock = Script.ControlBlock.build(internalPubkey, merkleRoot, listOf(leaves[0], leaves[2]))
val hash = hashForSigningSchnorr(tmp, 0, listOf(fundingTx2.txOut[0]), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(null, ScriptTree.hash(leaves[1])))
val sig = Crypto.signSchnorr(hash, privs[1], Crypto.SchnorrTweak.NoTweak) // signature for script spend of leaf #2
tmp.updateWitness(0, ScriptWitness(listOf(sig, Script.write(scripts[1]).byteVector(), controlBlock.byteVector())))
Expand All @@ -340,9 +334,7 @@ class TaprootTestsCommon {
lockTime = 0
)
// to re-compute the merkle root we need to provide branch(#1, #2)
val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) +
internalPubkey.value.toByteArray() +
ScriptTree.hash(ScriptTree.Branch(leaves[0], leaves[1])).toByteArray()
val controlBlock = Script.ControlBlock.build(internalPubkey, merkleRoot, listOf(ScriptTree.Branch(leaves[0], leaves[1])))
val hash = hashForSigningSchnorr(tmp, 0, listOf(fundingTx3.txOut[1]), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(null, ScriptTree.hash(leaves[2])))
val sig = Crypto.signSchnorr(hash, privs[2], Crypto.SchnorrTweak.NoTweak) // signature for script spend of leaf #3
tmp.updateWitness(0, ScriptWitness(listOf(sig, Script.write(scripts[2]).byteVector(), controlBlock.byteVector())))
Expand Down

0 comments on commit 92223a9

Please sign in to comment.