diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt index 71c1c374..4ecb8fd3 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt @@ -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) } @@ -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> = 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, @@ -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() @@ -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() @@ -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() @@ -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) @@ -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 -> {} @@ -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 -> {} @@ -1299,6 +1338,7 @@ 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()) @@ -1306,6 +1346,7 @@ public object Script { 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) @@ -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 @@ -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 } @@ -1438,6 +1481,7 @@ public object Script { verifyWitnessProgram(witness, witnessVersion.toLong(), program.data.toByteArray(), isP2sh = false) stack0.take(1) } + else -> stack0 } } else stack0 @@ -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 diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/ScriptTree.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/ScriptTree.kt index 9d663b04..f003c1d4 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/ScriptTree.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/ScriptTree.kt @@ -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, leafVersion: Int) : this(id, Script.write(script).byteVector(), leafVersion) /** * tapleaf hash of this leaf */ diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt index c26fb653..00d69e94 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt @@ -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 @@ -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 = 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) + } + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt index 41042d69..40830838 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt @@ -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) @@ -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()))) @@ -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() @@ -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()))) @@ -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()))) @@ -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())))