diff --git a/build.gradle.kts b/build.gradle.kts index b47427bb..9eba497c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { val currentOs = org.gradle.internal.os.OperatingSystem.current() group = "fr.acinq.bitcoin" -version = "0.14.0" +version = "0.15.0-SNAPSHOT" repositories { google() 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/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt new file mode 100644 index 00000000..9640ff04 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt @@ -0,0 +1,294 @@ +package fr.acinq.bitcoin.musig2 + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.secp256k1.Hex +import fr.acinq.secp256k1.Secp256k1 +import kotlin.experimental.xor +import kotlin.jvm.JvmStatic + + +/** + * Key Aggregation Context + * Holds a public key aggregate that can optionally be tweaked + * @param Q aggregated public key + * @param gacc G accumulator + * @param tacc tweak accumulator + */ +public data class KeyAggCtx(val Q: PublicKey, val gacc: Boolean, val tacc: ByteVector32) { + public fun tweak(tweak: ByteVector32, isXonly: Boolean): KeyAggCtx { + require(tweak == ByteVector32.Zeroes || PrivateKey(tweak).isValid()) { "invalid tweak" } + return if (isXonly && !Q.isEven()) { + val Q1 = PublicKey.parse(Secp256k1.pubKeyTweakAdd(Q.unaryMinus().toUncompressedBin(), tweak.toByteArray())) + KeyAggCtx(Q1, !gacc, minus(tweak, tacc)) + } else { + val Q1 = PublicKey.parse(Secp256k1.pubKeyTweakAdd(Q.toUncompressedBin(), tweak.toByteArray())) + KeyAggCtx(Q1, gacc, add(tweak, tacc)) + } + } +} + +public object Musig2 { + @JvmStatic + public fun keyAgg(pubkeys: List): KeyAggCtx { + val pk2 = getSecondKey(pubkeys) + val a = pubkeys.map { keyAggCoeffInternal(pubkeys, it, pk2) } + val Q = pubkeys.zip(a).map { it.first.times(PrivateKey(it.second)) }.reduce { p1, p2 -> p1 + p2 } + return KeyAggCtx(Q, true, ByteVector32.Zeroes) + } + + @JvmStatic + public fun keySort(pubkeys: List): List = pubkeys.sortedWith { a, b -> LexicographicalOrdering.compare(a, b) } +} + +/** + * Musig2 secret nonce. Not meant to be reused !! + */ +public data class SecretNonce(val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + + public constructor(hex: String) : this(Hex.decode(hex)) + + init { + require(data.size() == 32 + 32 + 33) { "musig2 secret nonce must be 97 bytes" } + } + + internal val p1: PrivateKey = PrivateKey(data.take(32)) + internal val p2: PrivateKey = PrivateKey(data.drop(32).take(32)) + internal val pk: PublicKey = PublicKey(data.takeRight(33)) + public fun publicNonce(): IndividualNonce = IndividualNonce(p1.publicKey().value + p2.publicKey().value) + + public companion object { + /** + * @param sk optional private key + * @param pk public key + * @param aggpk optional aggregated public key + * @param msg optional message + * @param extraInput optional extra input + * @param randprime random value + * @return a Musig2 secret nonce + */ + @JvmStatic + public fun generate(sk: PrivateKey?, pk: PublicKey, aggpk: XonlyPublicKey?, msg: ByteArray?, extraInput: ByteArray?, randprime: ByteVector32): SecretNonce { + + fun xor(a: ByteVector32, b: ByteVector32): ByteVector32 { + val result = ByteArray(32) + for (i in 0..31) { + result[i] = a[i].xor(b[i]) + } + return result.byteVector32() + } + + val rand = if (sk != null) { + xor(sk.value, Crypto.taggedHash(randprime.toByteArray(), "MuSig/aux")) + } else { + randprime + } + val aggpk1 = aggpk?.value?.toByteArray() ?: ByteArray(0) + val extraInput1 = extraInput ?: ByteArray(0) + val tmp = rand.toByteArray() + + ByteArray(1) { pk.value.size().toByte() } + pk.value.toByteArray() + + ByteArray(1) { aggpk1.size.toByte() } + aggpk1 + + if (msg != null) { + ByteArray(1) { 1 } + Pack.writeInt64BE(msg.size.toLong()) + msg + } else { + ByteArray(1) { 0 } + } + + Pack.writeInt32BE(extraInput1.size) + extraInput1 + val k1 = Crypto.taggedHash(tmp + ByteArray(1) { 0 }, "MuSig/nonce") + require(k1 != ByteVector32.Zeroes) + val k2 = Crypto.taggedHash(tmp + ByteArray(1) { 1 }, "MuSig/nonce") + require(k2 != ByteVector32.Zeroes) + val secnonce = SecretNonce(PrivateKey(k1).value + PrivateKey(k2).value + pk.value) + return secnonce + } + } +} + +/** + * Musig2 public nonce + */ +public data class IndividualNonce(val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + + public constructor(hex: String) : this(Hex.decode(hex)) + + init { + require(data.size() == 66) { "individual musig2 public nonce must be 66 bytes" } + } + + internal val P1: PublicKey = PublicKey(data.take(33)) + + internal val P2: PublicKey = PublicKey(data.drop(33)) + public fun isValid(): Boolean = P1.isValid() && P2.isValid() + + public fun toByteArray(): ByteArray = data.toByteArray() + + public companion object { + @JvmStatic + public fun aggregate(nonces: List): AggregatedNonce { + for (i in nonces.indices) { + require(nonces[i].isValid()) { "invalid nonce at index $i" } + } + val np: PublicKey? = null + val R1 = nonces.map { it.P1 }.fold(np) { a, b -> add(a, b) } + val R2 = nonces.map { it.P2 }.fold(np) { a, b -> add(a, b) } + return AggregatedNonce(R1, R2) + } + } +} + +/** + * Aggregated nonce. + * The sum of 2 public keys could be 0 (P + (-P)) which we represent with null (0 is a valid point but not a valid public key) + */ +public data class AggregatedNonce(val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + + public constructor(hex: String) : this(Hex.decode(hex)) + + internal constructor(p1: PublicKey?, p2: PublicKey?) : this((p1?.value?.toByteArray() ?: ByteArray(33)) + (p2?.value?.toByteArray() ?: ByteArray(33))) + + init { + require(data.size() == 66) { "aggregated musig2 public nonce must be 66 bytes" } + } + + internal val P1: PublicKey? = run { + val bin = data.take(33) + if (bin.contentEquals(ByteArray(33))) null else PublicKey(bin) + } + + internal val P2: PublicKey? = run { + val bin = data.drop(33) + if (bin.contentEquals(ByteArray(33))) null else PublicKey(bin) + } + + public fun isValid(): Boolean = (P1?.isValid() ?: true) && (P2?.isValid() ?: true) + + public fun toByteArray(): ByteArray = data.toByteArray() +} + +internal fun add(a: ByteVector32, b: ByteVector32): ByteVector32 = when { + a == ByteVector32.Zeroes -> b + b == ByteVector32.Zeroes -> a + else -> (PrivateKey(a) + PrivateKey(b)).value +} + +internal fun unaryMinus(a: ByteVector32): ByteVector32 = when { + a == ByteVector32.Zeroes -> a + else -> PrivateKey(a).unaryMinus().value +} + +internal fun minus(a: ByteVector32, b: ByteVector32): ByteVector32 = add(a, unaryMinus(b)) +internal fun mul(a: ByteVector32, b: ByteVector32): ByteVector32 = when { + a == ByteVector32.Zeroes || b == ByteVector32.Zeroes -> ByteVector32.Zeroes + else -> (PrivateKey(a) * PrivateKey(b)).value +} + +internal fun add(a: PublicKey?, b: PublicKey?): PublicKey? = when { + a == null -> b + b == null -> a + a.xOnly() == b.xOnly() && (a.isEven() != b.isEven()) -> null + else -> a + b +} + + +internal fun mul(a: PublicKey?, b: PrivateKey): PublicKey? = a?.times(b) + +/** + * Musig2 signing session context + * @param aggnonce aggregated public nonce + * @param pubkeys signer public keys + * @param tweaks optional tweaks to apply to the aggregated public key + * @param message message to sign + */ +public data class SessionCtx(val aggnonce: AggregatedNonce, val pubkeys: List, val tweaks: List>, val message: ByteVector) { + private fun build(): SessionValues { + val keyAggCtx0 = Musig2.keyAgg(pubkeys) + val keyAggCtx = tweaks.fold(keyAggCtx0) { ctx, tweak -> ctx.tweak(tweak.first, tweak.second) } + val (Q, gacc, tacc) = keyAggCtx + val b = PrivateKey(Crypto.taggedHash((aggnonce.toByteArray().byteVector() + Q.xOnly().value + message).toByteArray(), "MuSig/noncecoef")) + val R = add(aggnonce.P1, mul(aggnonce.P2, b)) ?: PublicKey.Generator + val e = Crypto.taggedHash((R.xOnly().value + Q.xOnly().value + message).toByteArray(), "BIP0340/challenge") + return SessionValues(Q, gacc, tacc, b, R, PrivateKey(e)) + } + + private fun getSessionKeyAggCoeff(P: PublicKey): PrivateKey { + require(pubkeys.contains(P)) { "signer's pubkey is not present" } + return keyAggCoeff(pubkeys, P) + } + + /** + * @param secnonce secret nonce + * @param sk private key + * @return a Musig2 partial signature, or null if the nonce does not match the private key or the partial signature cannot be verified + */ + public fun sign(secnonce: SecretNonce, sk: PrivateKey): ByteVector32? = runCatching { + val (Q, gacc, _, b, R, e) = build() + val (k1, k2) = if (R.isEven()) Pair(secnonce.p1, secnonce.p2) else Pair(-secnonce.p1, -secnonce.p2) + val P = sk.publicKey() + require(P == secnonce.pk) { "nonce and private key mismatch" } + val a = getSessionKeyAggCoeff(P) + val d = if (Q.isEven() == gacc) sk else -sk + val s = k1 + b * k2 + e * a * d + require(partialSigVerify(s.value, secnonce.publicNonce(), sk.publicKey())) { "partial signature verification failed" } + s.value + }.getOrNull() + + /** + * @param psig Musig2 partial signature + * @param pubnonce public nonce + * @param pk public key + * @return true if the partial signature has been verified (in the context of a specific signing session) + */ + public fun partialSigVerify(psig: ByteVector32, pubnonce: IndividualNonce, pk: PublicKey): Boolean { + val (Q, gacc, _, b, R, e) = build() + val Rstar = add(pubnonce.P1, mul(pubnonce.P2, b)) ?: PublicKey.Generator + val Re = if (R.isEven()) Rstar else -Rstar + val a = getSessionKeyAggCoeff(pk) + val gprime = if (Q.isEven()) gacc else !gacc + val check = if (gprime) Re + pk * e * a else Re - pk * e * a + return PrivateKey(psig).publicKey() == check + } + + /** + * @param psigs list of partial signatures + * @return an aggregated signature, which is a valid Schnorr signature for the matching aggregated public key + * or null is one of the partial signatures is not valid + */ + public fun partialSigAgg(psigs: List): ByteVector64? = runCatching { + val (Q, _, tacc, _, R, e) = build() + for (i in psigs.indices) { + require(PrivateKey(psigs[i]).isValid()) { "invalid partial signature at index $i" } + } + val s = psigs.reduce { a, b -> add(a, b) } + val s1 = if (Q.isEven()) add(s, mul(e.value, tacc)) else minus(s, mul(e.value, tacc)) + val sig = ByteVector64(R.xOnly().value + s1) + sig + }.getOrNull() + + public companion object { + private data class SessionValues(val Q: PublicKey, val gacc: Boolean, val tacc: ByteVector32, val b: PrivateKey, val R: PublicKey, val e: PrivateKey) + } +} + +internal fun getSecondKey(pubkeys: List): PublicKey { + return pubkeys.drop(1).find { it != pubkeys[0] } ?: PublicKey(ByteArray(33)) +} + +internal fun hashKeys(pubkeys: List): ByteVector32 { + val concat = pubkeys.map { it.value }.reduce { a, b -> a + b } + return Crypto.taggedHash(concat.toByteArray(), "KeyAgg list") +} + +internal fun keyAggCoeffInternal(pubkeys: List, pk: PublicKey, pk2: PublicKey): ByteVector32 { + return if (pk == pk2) { + ByteVector32.One.reversed() + } else { + Crypto.taggedHash(hashKeys(pubkeys).toByteArray() + pk.value.toByteArray(), "KeyAgg coefficient") + } +} + +internal fun keyAggCoeff(pubkeys: List, pk: PublicKey): PrivateKey { + return PrivateKey(keyAggCoeffInternal(pubkeys, pk, getSecondKey(pubkeys))) +} diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt new file mode 100644 index 00000000..3dccb426 --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt @@ -0,0 +1,397 @@ +package fr.acinq.bitcoin + +import fr.acinq.bitcoin.musig2.* +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 +import kotlin.test.assertTrue + +class Musig2TestsCommon { + @Test + fun `sort public keys`() { + val tests = TransactionTestsCommon.readData("musig2/key_sort_vectors.json") + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val expected = tests.jsonObject["sorted_pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + assertEquals(expected, Musig2.keySort(pubkeys)) + } + + @Test + fun `aggregate public keys`() { + val tests = TransactionTestsCommon.readData("musig2/key_agg_vectors.json") + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = XonlyPublicKey(ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content)) + val ctx = Musig2.keyAgg(keyIndices.map { pubkeys[it] }) + assertEquals(expected, ctx.Q.xOnly()) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + assertFails { + var ctx = Musig2.keyAgg(keyIndices.map { pubkeys[it] }) + tweakIndices.zip(isXonly).forEach { ctx = ctx.tweak(tweaks[it.first], it.second) } + } + } + } + + @Test + fun `generate secret nonce`() { + val tests = TransactionTestsCommon.readData("musig2/nonce_gen_vectors.json") + tests.jsonObject["test_cases"]!!.jsonArray.forEach { + val randprime = ByteVector32.fromValidHex(it.jsonObject["rand_"]!!.jsonPrimitive.content) + val sk = it.jsonObject["sk"]?.jsonPrimitive?.contentOrNull?.let { PrivateKey.fromHex(it) } + val pk = PublicKey.fromHex(it.jsonObject["pk"]!!.jsonPrimitive.content) + val aggpk = it.jsonObject["aggpk"]?.jsonPrimitive?.contentOrNull?.let { XonlyPublicKey(ByteVector32.fromValidHex(it)) } + val msg = it.jsonObject["msg"]?.jsonPrimitive?.contentOrNull?.let { Hex.decode(it) } + val extraInput = it.jsonObject["extra_in"]?.jsonPrimitive?.contentOrNull?.let { Hex.decode(it) } + val expectedSecnonce = SecretNonce(it.jsonObject["expected_secnonce"]!!.jsonPrimitive.content) + val expectedPubnonce = IndividualNonce(it.jsonObject["expected_pubnonce"]!!.jsonPrimitive.content) + val secnonce = SecretNonce.generate(sk, pk, aggpk, msg, extraInput, randprime) + assertEquals(expectedSecnonce, secnonce) + assertEquals(expectedPubnonce, secnonce.publicNonce()) + } + } + + @Test + fun `aggregate nonces`() { + val tests = TransactionTestsCommon.readData("musig2/nonce_agg_vectors.json") + val nonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = AggregatedNonce(it.jsonObject["expected"]!!.jsonPrimitive.content) + val agg = IndividualNonce.aggregate(nonceIndices.map { nonces[it] }) + assertEquals(expected, agg) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + assertFails { + IndividualNonce.aggregate(nonceIndices.map { nonces[it] }) + } + } + } + + @Test + fun `sign`() { + val tests = TransactionTestsCommon.readData("musig2/sign_verify_vectors.json") + val sk = PrivateKey.fromHex(tests.jsonObject["sk"]!!.jsonPrimitive.content) + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val secnonces = tests.jsonObject["secnonces"]!!.jsonArray.map { SecretNonce(it.jsonPrimitive.content) } + val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + val aggnonces = tests.jsonObject["aggnonces"]!!.jsonArray.map { AggregatedNonce(it.jsonPrimitive.content) } + val msgs = tests.jsonObject["msgs"]!!.jsonArray.map { ByteVector(it.jsonPrimitive.content) } + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + val agg = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }) + assertEquals(aggnonces[it.jsonObject["aggnonce_index"]!!.jsonPrimitive.int], agg) + val ctx = SessionCtx( + agg, + keyIndices.map { pubkeys[it] }, + listOf(), + msgs[it.jsonObject["msg_index"]!!.jsonPrimitive.int] + ) + val psig = ctx.sign(secnonces[keyIndices[signerIndex]], sk)!! + assertEquals(expected, psig) + assertTrue { + ctx.partialSigVerify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]]) + } + } + + tests.jsonObject["sign_error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val ctx = SessionCtx( + aggnonces[it.jsonObject["aggnonce_index"]!!.jsonPrimitive.int], + keyIndices.map { pubkeys[it] }, + listOf(), + msgs[it.jsonObject["msg_index"]!!.jsonPrimitive.int] + ) + require(ctx.sign(secnonces[it.jsonObject["secnonce_index"]!!.jsonPrimitive.int], sk) == null) + } + } + + @Test + fun `aggregate signatures`() { + val tests = TransactionTestsCommon.readData("musig2/sig_agg_vectors.json") + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + val psigs = tests.jsonObject["psigs"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + val msg = ByteVector.fromHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val psigIndices = it.jsonObject["psig_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = ByteVector64.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) + val ctx = SessionCtx( + aggnonce, + keyIndices.map { pubkeys[it] }, + tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, + msg + ) + val aggsig = ctx.partialSigAgg(psigIndices.map { psigs[it] })!! + assertEquals(expected, aggsig) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val psigIndices = it.jsonObject["psig_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) + val ctx = SessionCtx( + aggnonce, + keyIndices.map { pubkeys[it] }, + tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, + msg + ) + require(ctx.partialSigAgg(psigIndices.map { psigs[it] }) == null) + } + } + + @Test + fun `tweak tests`() { + val tests = TransactionTestsCommon.readData("musig2/tweak_vectors.json") + val sk = PrivateKey.fromHex(tests.jsonObject["sk"]!!.jsonPrimitive.content) + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + val msg = ByteVector.fromHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) + + val secnonce = SecretNonce(tests.jsonObject["secnonce"]!!.jsonPrimitive.content) + val aggnonce = AggregatedNonce(tests.jsonObject["aggnonce"]!!.jsonPrimitive.content) + + assertEquals(pubkeys[0], sk.publicKey()) + assertEquals(pnonces[0], secnonce.publicNonce()) + assertEquals(aggnonce, IndividualNonce.aggregate(listOf(pnonces[0], pnonces[1], pnonces[2]))) + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) + assertEquals(aggnonce, IndividualNonce.aggregate(nonceIndices.map { pnonces[it] })) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + val ctx = SessionCtx( + aggnonce, + keyIndices.map { pubkeys[it] }, + tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, + msg + ) + val psig = ctx.sign(secnonce, sk)!! + assertEquals(expected, psig) + assertTrue { ctx.partialSigVerify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]]) } + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + assertEquals(aggnonce, IndividualNonce.aggregate(nonceIndices.map { pnonces[it] })) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + assertFails { + val ctx = SessionCtx( + aggnonce, + keyIndices.map { pubkeys[it] }, + tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, + msg + ) + val psig = ctx.sign(secnonce, sk)!! + ctx.partialSigVerify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]]) + } + } + } + + @Test + fun `simple musig2 example`() { + val random = kotlin.random.Random.Default + val msg = random.nextBytes(32).byteVector32() + + val privkeys = listOf( + PrivateKey(ByteArray(32) { 1 }), + PrivateKey(ByteArray(32) { 2 }), + PrivateKey(ByteArray(32) { 3 }), + ) + val pubkeys = privkeys.map { it.publicKey() } + + val plainTweak = ByteVector32("this could be a BIP32 tweak....".encodeToByteArray() + ByteArray(1)) + val xonlyTweak = ByteVector32("this could be a taproot tweak..".encodeToByteArray() + ByteArray(1)) + + val aggsig = run { + val secnonces = privkeys.map { + SecretNonce.generate(it, it.publicKey(), null, null, null, random.nextBytes(32).byteVector32()) + } + + val pubnonces = secnonces.map { it.publicNonce() } + + // aggregate public nonces + val aggnonce = IndividualNonce.aggregate(pubnonces) + + // create a signing session + val ctx = SessionCtx( + aggnonce, + pubkeys, + listOf(Pair(plainTweak, false), Pair(xonlyTweak, true)), + msg + ) + + // create partial signatures + val psigs = privkeys.indices.map { + ctx.sign(secnonces[it], privkeys[it])!! + } + + // verify partial signatures + pubkeys.indices.forEach { + assertTrue(ctx.partialSigVerify(psigs[it], pubnonces[it], pubkeys[it])) + } + + // aggregate partial signatures + ctx.partialSigAgg(psigs)!! + } + + // aggregate public keys + val aggpub = Musig2.keyAgg(pubkeys) + .tweak(plainTweak, false) + .tweak(xonlyTweak, true) + + // check that the aggregated signature is a valid, plain Schnorr signature for the aggregated public key + assertTrue(Crypto.verifySignatureSchnorr(msg, aggsig, aggpub.Q.xOnly())) + } + + @Test + fun `use musig2 to replace multisig 2-of-2`() { + val alicePrivKey = PrivateKey(ByteArray(32) { 1 }) + val alicePubKey = alicePrivKey.publicKey() + val bobPrivKey = PrivateKey(ByteArray(32) { 2 }) + val bobPubKey = bobPrivKey.publicKey() + + // Alice and Bob exchange public keys and agree on a common aggregated key + val internalPubKey = Musig2.keyAgg(listOf(alicePubKey, bobPubKey)).Q.xOnly() + // we use the standard BIP86 tweak + val commonPubKey = internalPubKey.outputKey(Crypto.TaprootTweak.NoScriptTweak).first + + // this tx sends to a standard p2tr(commonPubKey) script + val tx = Transaction(2, listOf(), listOf(TxOut(Satoshi(10000), Script.pay2tr(commonPubKey))), 0) + + // this is how Alice and Bob would spend that tx + val spendingTx = Transaction(2, listOf(TxIn(OutPoint(tx, 0), sequence = 0)), listOf(TxOut(Satoshi(10000), Script.pay2wpkh(alicePubKey))), 0) + + val commonSig = run { + val random = kotlin.random.Random.Default + val aliceNonce = SecretNonce.generate(alicePrivKey, alicePubKey, commonPubKey, null, null, random.nextBytes(32).byteVector32()) + val bobNonce = SecretNonce.generate(bobPrivKey, bobPubKey, commonPubKey, null, null, random.nextBytes(32).byteVector32()) + + val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.publicNonce(), bobNonce.publicNonce())) + val msg = Transaction.hashForSigningSchnorr(spendingTx, 0, listOf(tx.txOut[0]), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + + // we use the same ctx for Alice and Bob, they both know all the public keys that are used here + val ctx = SessionCtx( + aggnonce, + listOf(alicePubKey, bobPubKey), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true)), + msg + ) + val aliceSig = ctx.sign(aliceNonce, alicePrivKey)!! + val bobSig = ctx.sign(bobNonce, bobPrivKey)!! + ctx.partialSigAgg(listOf(aliceSig, bobSig))!! + } + + // this tx looks like any other tx that spends a p2tr output, with a single signature + 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 = IndividualNonce.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 6d133aaf..d6a92572 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() @@ -294,10 +294,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()))) @@ -326,10 +323,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()))) @@ -357,9 +351,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()))) diff --git a/src/commonTest/resources/musig2/det_sign_vectors.json b/src/commonTest/resources/musig2/det_sign_vectors.json new file mode 100644 index 00000000..261669cc --- /dev/null +++ b/src/commonTest/resources/musig2/det_sign_vectors.json @@ -0,0 +1,144 @@ +{ + "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "msgs": [ + "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "2626262626262626262626262626262626262626262626262626262626262626262626262626" + ], + "valid_test_cases": [ + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [0, 1, 2], + "tweaks": [], + "is_xonly": [], + "msg_index": 0, + "signer_index": 0, + "expected": [ + "03D96275257C2FCCBB6EEB77BDDF51D3C88C26EE1626C6CDA8999B9D34F4BA13A60309BE2BF883C6ABE907FA822D9CA166D51A3DCC28910C57528F6983FC378B7843", + "41EA65093F71D084785B20DC26A887CD941C9597860A21660CBDB9CC2113CAD3" + ] + }, + { + "rand": null, + "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [1, 0, 2], + "tweaks": [], + "is_xonly": [], + "msg_index": 0, + "signer_index": 1, + "expected": [ + "028FBCCF5BB73A7B61B270BAD15C0F9475D577DD85C2157C9D38BEF1EC922B48770253BE3638C87369BC287E446B7F2C8CA5BEB9FFBD1EA082C62913982A65FC214D", + "AEAA31262637BFA88D5606679018A0FEEEC341F3107D1199857F6C81DE61B8DD" + ] + }, + { + "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "aggothernonce": "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "key_indices": [1, 2, 0], + "tweaks": [], + "is_xonly": [], + "msg_index": 1, + "signer_index": 2, + "expected": [ + "024FA8D774F0C8743FAA77AFB4D08EE5A013C2E8EEAD8A6F08A77DDD2D28266DB803050905E8C994477F3F2981861A2E3791EF558626E645FBF5AA131C5D6447C2C2", + "FEE28A56B8556B7632E42A84122C51A4861B1F2DEC7E81B632195E56A52E3E13" + ], + "comment": "Message longer than 32 bytes" + }, + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046", + "key_indices": [0, 1, 2], + "tweaks": ["E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB"], + "is_xonly": [true], + "msg_index": 0, + "signer_index": 0, + "expected": [ + "031E07C0D11A0134E55DB1FC16095ADCBD564236194374AA882BFB3C78273BF673039D0336E8CA6288C00BFC1F8B594563529C98661172B9BC1BE85C23A4CE1F616B", + "7B1246C5889E59CB0375FA395CC86AC42D5D7D59FD8EAB4FDF1DCAB2B2F006EA" + ], + "comment": "Tweaked public key" + } + ], + "error_test_cases": [ + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [1, 0, 3], + "tweaks": [], + "is_xonly": [], + "msg_index": 0, + "signer_index": 1, + "error": { + "type": "invalid_contribution", + "signer": 2, + "contrib": "pubkey" + }, + "comment": "Signer 2 provided an invalid public key" + }, + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [1, 2], + "tweaks": [], + "is_xonly": [], + "msg_index": 0, + "signer_index": 1, + "error": { + "type": "value", + "message": "The signer's pubkey must be included in the list of pubkeys." + }, + "comment": "The signers pubkey is not in the list of pubkeys" + }, + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "0437C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [1, 2, 0], + "tweaks": [], + "is_xonly": [], + "msg_index": 0, + "signer_index": 2, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggothernonce" + }, + "comment": "aggothernonce is invalid due wrong tag, 0x04, in the first half" + }, + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [1, 2, 0], + "tweaks": [], + "is_xonly": [], + "msg_index": 0, + "signer_index": 2, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggothernonce" + }, + "comment": "aggothernonce is invalid because first half corresponds to point at infinity" + }, + { + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "key_indices": [1, 2, 0], + "tweaks": ["FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"], + "is_xonly": [false], + "msg_index": 0, + "signer_index": 2, + "error": { + "type": "value", + "message": "The tweak must be less than n." + }, + "comment": "Tweak is invalid because it exceeds group size" + } + ] +} diff --git a/src/commonTest/resources/musig2/key_agg_vectors.json b/src/commonTest/resources/musig2/key_agg_vectors.json new file mode 100644 index 00000000..b2e623de --- /dev/null +++ b/src/commonTest/resources/musig2/key_agg_vectors.json @@ -0,0 +1,88 @@ +{ + "pubkeys": [ + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "020000000000000000000000000000000000000000000000000000000000000005", + "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" + ], + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B" + ], + "valid_test_cases": [ + { + "key_indices": [0, 1, 2], + "expected": "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C" + }, + { + "key_indices": [2, 1, 0], + "expected": "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B" + }, + { + "key_indices": [0, 0, 0], + "expected": "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935" + }, + { + "key_indices": [0, 0, 1, 1], + "expected": "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E" + } + ], + "error_test_cases": [ + { + "key_indices": [0, 3], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubkey" + }, + "comment": "Invalid public key" + }, + { + "key_indices": [0, 4], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubkey" + }, + "comment": "Public key exceeds field size" + }, + { + "key_indices": [5, 0], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubkey" + }, + "comment": "First byte of public key is not 2 or 3" + }, + { + "key_indices": [0, 1], + "tweak_indices": [0], + "is_xonly": [true], + "error": { + "type": "value", + "message": "The tweak must be less than n." + }, + "comment": "Tweak is out of range" + }, + { + "key_indices": [6], + "tweak_indices": [1], + "is_xonly": [false], + "error": { + "type": "value", + "message": "The result of tweaking cannot be infinity." + }, + "comment": "Intermediate tweaking result is point at infinity" + } + ] +} diff --git a/src/commonTest/resources/musig2/key_sort_vectors.json b/src/commonTest/resources/musig2/key_sort_vectors.json new file mode 100644 index 00000000..de088a74 --- /dev/null +++ b/src/commonTest/resources/musig2/key_sort_vectors.json @@ -0,0 +1,18 @@ +{ + "pubkeys": [ + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8" + ], + "sorted_pubkeys": [ + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + ] +} diff --git a/src/commonTest/resources/musig2/nonce_agg_vectors.json b/src/commonTest/resources/musig2/nonce_agg_vectors.json new file mode 100644 index 00000000..1c04b881 --- /dev/null +++ b/src/commonTest/resources/musig2/nonce_agg_vectors.json @@ -0,0 +1,51 @@ +{ + "pnonces": [ + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" + ], + "valid_test_cases": [ + { + "pnonce_indices": [0, 1], + "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8" + }, + { + "pnonce_indices": [2, 3], + "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B000000000000000000000000000000000000000000000000000000000000000000", + "comment": "Sum of second points encoded in the nonces is point at infinity which is serialized as 33 zero bytes" + } + ], + "error_test_cases": [ + { + "pnonce_indices": [0, 4], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubnonce" + }, + "comment": "Public nonce from signer 1 is invalid due wrong tag, 0x04, in the first half" + }, + { + "pnonce_indices": [5, 1], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubnonce" + }, + "comment": "Public nonce from signer 0 is invalid because the second half does not correspond to an X coordinate" + }, + { + "pnonce_indices": [6, 1], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubnonce" + }, + "comment": "Public nonce from signer 0 is invalid because second half exceeds field size" + } + ] +} diff --git a/src/commonTest/resources/musig2/nonce_gen_vectors.json b/src/commonTest/resources/musig2/nonce_gen_vectors.json new file mode 100644 index 00000000..ced946f3 --- /dev/null +++ b/src/commonTest/resources/musig2/nonce_gen_vectors.json @@ -0,0 +1,44 @@ +{ + "test_cases": [ + { + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", + "msg": "0101010101010101010101010101010101010101010101010101010101010101", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected_secnonce": "B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB6495B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "expected_pubnonce": "02F7BE7089E8376EB355272368766B17E88E7DB72047D05E56AA881EA52B3B35DF02C29C8046FDD0DED4C7E55869137200FBDBFE2EB654267B6D7013602CAED3115A" + }, + { + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", + "msg": "", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected_secnonce": "E862B068500320088138468D47E0E6F147E01B6024244AE45EAC40ACE5929B9F0789E051170B9E705D0B9EB49049A323BBBBB206D8E05C19F46C6228742AA7A9024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "expected_pubnonce": "023034FA5E2679F01EE66E12225882A7A48CC66719B1B9D3B6C4DBD743EFEDA2C503F3FD6F01EB3A8E9CB315D73F1F3D287CAFBB44AB321153C6287F407600205109" + }, + { + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected_secnonce": "3221975ACBDEA6820EABF02A02B7F27D3A8EF68EE42787B88CBEFD9AA06AF3632EE85B1A61D8EF31126D4663A00DD96E9D1D4959E72D70FE5EBB6E7696EBA66F024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "expected_pubnonce": "02E5BBC21C69270F59BD634FCBFA281BE9D76601295345112C58954625BF23793A021307511C79F95D38ACACFF1B4DA98228B77E65AA216AD075E9673286EFB4EAF3" + }, + { + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": null, + "pk": "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "aggpk": null, + "msg": null, + "extra_in": null, + "expected_secnonce": "89BDD787D0284E5E4D5FC572E49E316BAB7E21E3B1830DE37DFE80156FA41A6D0B17AE8D024C53679699A6FD7944D9C4A366B514BAF43088E0708B1023DD289702F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "expected_pubnonce": "02C96E7CB1E8AA5DAC64D872947914198F607D90ECDE5200DE52978AD5DED63C000299EC5117C2D29EDEE8A2092587C3909BE694D5CFF0667D6C02EA4059F7CD9786" + } + ] +} diff --git a/src/commonTest/resources/musig2/sig_agg_vectors.json b/src/commonTest/resources/musig2/sig_agg_vectors.json new file mode 100644 index 00000000..04a7bc6b --- /dev/null +++ b/src/commonTest/resources/musig2/sig_agg_vectors.json @@ -0,0 +1,151 @@ +{ + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02D2DC6F5DF7C56ACF38C7FA0AE7A759AE30E19B37359DFDE015872324C7EF6E05", + "03C7FB101D97FF930ACD0C6760852EF64E69083DE0B06AC6335724754BB4B0522C", + "02352433B21E7E05D3B452B81CAE566E06D2E003ECE16D1074AABA4289E0E3D581" + ], + "pnonces": [ + "036E5EE6E28824029FEA3E8A9DDD2C8483F5AF98F7177C3AF3CB6F47CAF8D94AE902DBA67E4A1F3680826172DA15AFB1A8CA85C7C5CC88900905C8DC8C328511B53E", + "03E4F798DA48A76EEC1C9CC5AB7A880FFBA201A5F064E627EC9CB0031D1D58FC5103E06180315C5A522B7EC7C08B69DCD721C313C940819296D0A7AB8E8795AC1F00", + "02C0068FD25523A31578B8077F24F78F5BD5F2422AFF47C1FADA0F36B3CEB6C7D202098A55D1736AA5FCC21CF0729CCE852575C06C081125144763C2C4C4A05C09B6", + "031F5C87DCFBFCF330DEE4311D85E8F1DEA01D87A6F1C14CDFC7E4F1D8C441CFA40277BF176E9F747C34F81B0D9F072B1B404A86F402C2D86CF9EA9E9C69876EA3B9", + "023F7042046E0397822C4144A17F8B63D78748696A46C3B9F0A901D296EC3406C302022B0B464292CF9751D699F10980AC764E6F671EFCA15069BBE62B0D1C62522A", + "02D97DDA5988461DF58C5897444F116A7C74E5711BF77A9446E27806563F3B6C47020CBAD9C363A7737F99FA06B6BE093CEAFF5397316C5AC46915C43767AE867C00" + ], + "tweaks": [ + "B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C", + "A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC", + "75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8" + ], + "psigs": [ + "B15D2CD3C3D22B04DAE438CE653F6B4ECF042F42CFDED7C41B64AAF9B4AF53FB", + "6193D6AC61B354E9105BBDC8937A3454A6D705B6D57322A5A472A02CE99FCB64", + "9A87D3B79EC67228CB97878B76049B15DBD05B8158D17B5B9114D3C226887505", + "66F82EA90923689B855D36C6B7E032FB9970301481B99E01CDB4D6AC7C347A15", + "4F5AEE41510848A6447DCD1BBC78457EF69024944C87F40250D3EF2C25D33EFE", + "DDEF427BBB847CC027BEFF4EDB01038148917832253EBC355FC33F4A8E2FCCE4", + "97B890A26C981DA8102D3BC294159D171D72810FDF7C6A691DEF02F0F7AF3FDC", + "53FA9E08BA5243CBCB0D797C5EE83BC6728E539EB76C2D0BF0F971EE4E909971", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869", + "valid_test_cases": [ + { + "aggnonce": "0341432722C5CD0268D829C702CF0D1CBCE57033EED201FD335191385227C3210C03D377F2D258B64AADC0E16F26462323D701D286046A2EA93365656AFD9875982B", + "nonce_indices": [ + 0, + 1 + ], + "key_indices": [ + 0, + 1 + ], + "tweak_indices": [], + "is_xonly": [], + "psig_indices": [ + 0, + 1 + ], + "expected": "041DA22223CE65C92C9A0D6C2CAC828AAF1EEE56304FEC371DDF91EBB2B9EF0912F1038025857FEDEB3FF696F8B99FA4BB2C5812F6095A2E0004EC99CE18DE1E" + }, + { + "aggnonce": "0224AFD36C902084058B51B5D36676BBA4DC97C775873768E58822F87FE437D792028CB15929099EEE2F5DAE404CD39357591BA32E9AF4E162B8D3E7CB5EFE31CB20", + "nonce_indices": [ + 0, + 2 + ], + "key_indices": [ + 0, + 2 + ], + "tweak_indices": [], + "is_xonly": [], + "psig_indices": [ + 2, + 3 + ], + "expected": "1069B67EC3D2F3C7C08291ACCB17A9C9B8F2819A52EB5DF8726E17E7D6B52E9F01800260A7E9DAC450F4BE522DE4CE12BA91AEAF2B4279219EF74BE1D286ADD9" + }, + { + "aggnonce": "0208C5C438C710F4F96A61E9FF3C37758814B8C3AE12BFEA0ED2C87FF6954FF186020B1816EA104B4FCA2D304D733E0E19CEAD51303FF6420BFD222335CAA402916D", + "nonce_indices": [ + 0, + 3 + ], + "key_indices": [ + 0, + 2 + ], + "tweak_indices": [ + 0 + ], + "is_xonly": [ + false + ], + "psig_indices": [ + 4, + 5 + ], + "expected": "5C558E1DCADE86DA0B2F02626A512E30A22CF5255CAEA7EE32C38E9A71A0E9148BA6C0E6EC7683B64220F0298696F1B878CD47B107B81F7188812D593971E0CC" + }, + { + "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD", + "nonce_indices": [ + 0, + 4 + ], + "key_indices": [ + 0, + 3 + ], + "tweak_indices": [ + 0, + 1, + 2 + ], + "is_xonly": [ + true, + false, + true + ], + "psig_indices": [ + 6, + 7 + ], + "expected": "839B08820B681DBA8DAF4CC7B104E8F2638F9388F8D7A555DC17B6E6971D7426CE07BF6AB01F1DB50E4E33719295F4094572B79868E440FB3DEFD3FAC1DB589E" + } + ], + "error_test_cases": [ + { + "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD", + "nonce_indices": [ + 0, + 4 + ], + "key_indices": [ + 0, + 3 + ], + "tweak_indices": [ + 0, + 1, + 2 + ], + "is_xonly": [ + true, + false, + true + ], + "psig_indices": [ + 7, + 8 + ], + "error": { + "type": "invalid_contribution", + "signer": 1 + }, + "comment": "Partial signature is invalid because it exceeds group size" + } + ] +} diff --git a/src/commonTest/resources/musig2/sign_verify_vectors.json b/src/commonTest/resources/musig2/sign_verify_vectors.json new file mode 100644 index 00000000..b467640c --- /dev/null +++ b/src/commonTest/resources/musig2/sign_verify_vectors.json @@ -0,0 +1,212 @@ +{ + "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA661", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "secnonces": [ + "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" + ], + "pnonces": [ + "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046", + "0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480" + ], + "aggnonces": [ + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" + ], + "msgs": [ + "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "", + "2626262626262626262626262626262626262626262626262626262626262626262626262626" + ], + "valid_test_cases": [ + { + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 0, + "expected": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB" + }, + { + "key_indices": [1, 0, 2], + "nonce_indices": [1, 0, 2], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 1, + "expected": "9FF2F7AAA856150CC8819254218D3ADEEB0535269051897724F9DB3789513A52" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 2, + "expected": "FA23C359F6FAC4E7796BB93BC9F0532A95468C539BA20FF86D7C76ED92227900" + }, + { + "key_indices": [0, 1], + "nonce_indices": [0, 3], + "aggnonce_index": 1, + "msg_index": 0, + "signer_index": 0, + "expected": "AE386064B26105404798F75DE2EB9AF5EDA5387B064B83D049CB7C5E08879531", + "comment": "Both halves of aggregate nonce correspond to point at infinity" + }, + { + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 1, + "signer_index": 0, + "expected": "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D", + "comment": "Empty message" + }, + { + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 2, + "signer_index": 0, + "expected": "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C", + "comment": "38-byte message" + } + ], + "sign_error_test_cases": [ + { + "key_indices": [1, 2], + "aggnonce_index": 0, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "value", + "message": "The signer's pubkey must be included in the list of pubkeys." + }, + "comment": "The signers pubkey is not in the list of pubkeys. This test case is optional: it can be skipped by implementations that do not check that the signer's pubkey is included in the list of pubkeys." + }, + { + "key_indices": [1, 0, 3], + "aggnonce_index": 0, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": 2, + "contrib": "pubkey" + }, + "comment": "Signer 2 provided an invalid public key" + }, + { + "key_indices": [1, 2, 0], + "aggnonce_index": 2, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggnonce" + }, + "comment": "Aggregate nonce is invalid due wrong tag, 0x04, in the first half" + }, + { + "key_indices": [1, 2, 0], + "aggnonce_index": 3, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggnonce" + }, + "comment": "Aggregate nonce is invalid because the second half does not correspond to an X coordinate" + }, + { + "key_indices": [1, 2, 0], + "aggnonce_index": 4, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggnonce" + }, + "comment": "Aggregate nonce is invalid because second half exceeds field size" + }, + { + "key_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 0, + "secnonce_index": 1, + "error": { + "type": "value", + "message": "first secnonce value is out of range." + }, + "comment": "Secnonce is invalid which may indicate nonce reuse" + } + ], + "verify_fail_test_cases": [ + { + "sig": "97AC833ADCB1AFA42EBF9E0725616F3C9A0D5B614F6FE283CEAAA37A8FFAF406", + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 0, + "comment": "Wrong signature (which is equal to the negation of valid signature)" + }, + { + "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 1, + "comment": "Wrong signer" + }, + { + "sig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 0, + "comment": "Signature exceeds group size" + } + ], + "verify_error_test_cases": [ + { + "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", + "key_indices": [0, 1, 2], + "nonce_indices": [4, 1, 2], + "msg_index": 0, + "signer_index": 0, + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubnonce" + }, + "comment": "Invalid pubnonce" + }, + { + "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", + "key_indices": [3, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 0, + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubkey" + }, + "comment": "Invalid pubkey" + } + ] +} diff --git a/src/commonTest/resources/musig2/tweak_vectors.json b/src/commonTest/resources/musig2/tweak_vectors.json new file mode 100644 index 00000000..d0a7cfe8 --- /dev/null +++ b/src/commonTest/resources/musig2/tweak_vectors.json @@ -0,0 +1,84 @@ +{ + "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + ], + "secnonce": "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "pnonces": [ + "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046" + ], + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "valid_test_cases": [ + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0], + "is_xonly": [true], + "signer_index": 2, + "expected": "E28A5C66E61E178C2BA19DB77B6CF9F7E2F0F56C17918CD13135E60CC848FE91", + "comment": "A single x-only tweak" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0], + "is_xonly": [false], + "signer_index": 2, + "expected": "38B0767798252F21BF5702C48028B095428320F73A4B14DB1E25DE58543D2D2D", + "comment": "A single plain tweak" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0, 1], + "is_xonly": [false, true], + "signer_index": 2, + "expected": "408A0A21C4A0F5DACAF9646AD6EB6FECD7F7A11F03ED1F48DFFF2185BC2C2408", + "comment": "A plain tweak followed by an x-only tweak" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "signer_index": 2, + "expected": "45ABD206E61E3DF2EC9E264A6FEC8292141A633C28586388235541F9ADE75435", + "comment": "Four tweaks: plain, plain, x-only, x-only." + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [true, false, true, false], + "signer_index": 2, + "expected": "B255FDCAC27B40C7CE7848E2D3B7BF5EA0ED756DA81565AC804CCCA3E1D5D239", + "comment": "Four tweaks: x-only, plain, x-only, plain. If an implementation prohibits applying plain tweaks after x-only tweaks, it can skip this test vector or return an error." + } + ], + "error_test_cases": [ + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [4], + "is_xonly": [false], + "signer_index": 2, + "error": { + "type": "value", + "message": "The tweak must be less than n." + }, + "comment": "Tweak is invalid because it exceeds group size" + } + ] +}