From 8b86e02a1a01c984f96e8e4258c8c8556b53455c Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 14 Dec 2023 17:26:38 +0100 Subject: [PATCH 1/7] Move prototype musig2 implementation to commonTest --- .../kotlin/fr/acinq/bitcoin/musig2proto}/Musig2.kt | 2 +- .../fr/acinq/bitcoin/{ => musig2proto}/Musig2TestsCommon.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{commonMain/kotlin/fr/acinq/bitcoin/musig2 => commonTest/kotlin/fr/acinq/bitcoin/musig2proto}/Musig2.kt (99%) rename src/commonTest/kotlin/fr/acinq/bitcoin/{ => musig2proto}/Musig2TestsCommon.kt (99%) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt similarity index 99% rename from src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt rename to src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt index 9640ff04..47ff39d2 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt @@ -1,4 +1,4 @@ -package fr.acinq.bitcoin.musig2 +package fr.acinq.bitcoin.musig2proto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2TestsCommon.kt similarity index 99% rename from src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt rename to src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2TestsCommon.kt index 0e66b577..3205f9a4 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2TestsCommon.kt @@ -1,6 +1,6 @@ -package fr.acinq.bitcoin +package fr.acinq.bitcoin.musig2proto -import fr.acinq.bitcoin.musig2.* +import fr.acinq.bitcoin.* import fr.acinq.bitcoin.reference.TransactionTestsCommon import fr.acinq.secp256k1.Hex import kotlinx.serialization.json.* From 018a49c1cf4191ba02b6f7d5b8480615c89864e5 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 14 Dec 2023 17:54:17 +0100 Subject: [PATCH 2/7] Add wrapper for secp256k1-kmp's musig2 methods --- build.gradle.kts | 4 +- .../fr/acinq/bitcoin/crypto/musig2/Musig2.kt | 171 +++++++++++ .../crypto/musig2/Musig2TestsCommon.kt | 288 ++++++++++++++++++ 3 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt create mode 100644 src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt diff --git a/build.gradle.kts b/build.gradle.kts index 2517e413..46c8eec5 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.17.0-SNAPSHOT" +version = "0.17.0-MUSIG2-SNAPSHOT" repositories { google() @@ -45,7 +45,7 @@ kotlin { } sourceSets { - val secp256k1KmpVersion = "0.13.0" + val secp256k1KmpVersion = "0.14.0-MUSIG2-SNAPSHOT" val commonMain by getting { dependencies { diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt new file mode 100644 index 00000000..8402cb75 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt @@ -0,0 +1,171 @@ +package fr.acinq.bitcoin.crypto.musig2 + +import fr.acinq.bitcoin.* +import fr.acinq.secp256k1.Hex +import fr.acinq.secp256k1.Secp256k1 +import kotlin.jvm.JvmStatic + +/** + * Musig2 key aggregation cache + * Keeps track of an aggregate of public keys, that can optionally be tweaked + */ +public data class KeyAggCache(val data: ByteVector) { + public constructor(data: ByteArray) : this(data.byteVector()) + + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) { "musig2 keyagg cache must be ${Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + /** + * @param tweak tweak to apply + * @param isXonly true if the tweak is an x-only tweak + * @return an updated cache, and the tweaked aggregated public key + */ + public fun tweak(tweak: ByteVector32, isXonly: Boolean): Pair { + val localCache = toByteArray() + val tweaked = if (isXonly) { + Secp256k1.musigPubkeyXonlyTweakAdd(localCache, tweak.toByteArray()) + } else { + Secp256k1.musigPubkeyTweakAdd(localCache, tweak.toByteArray()) + } + return Pair(KeyAggCache(localCache), PublicKey.parse(tweaked)) + } + + public companion object { + /** + * @param pubkeys public keys to aggregate + * @param cache an optional key aggregation cache + * @return a new (if cache was null) or updated cache, and the aggregated public key + */ + @JvmStatic + public fun add(pubkeys: List, cache: KeyAggCache?): Pair { + val localCache = cache?.data?.toByteArray() ?: ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + val aggkey = Secp256k1.musigPubkeyAgg(pubkeys.map { it.value.toByteArray() }.toTypedArray(), localCache) + return Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector())) + } + } +} + +/** + * Musig2 signing session + */ +public data class Session(val data: ByteVector) { + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) { "musig2 session must be ${Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + /** + * @param secretNonce secret nonce + * @param pk private key + * @param aggCache key aggregation cache + * @return a Musig2 partial signature + */ + public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): ByteVector32 { + return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32() + } + + /** + * @param psig musig2 partial signature + * @param pubnonce public nonce, that must match the secret nonce psig was generated with + * @param pubkey public key, that must match the private key psig was generated with + * @param cache key aggregation cache + * @return true if the partial signature is valid + */ + public fun verify(psig: ByteVector32, pubnonce: IndividualNonce, pubkey: PublicKey, cache: KeyAggCache): Boolean { + return Secp256k1.musigPartialSigVerify(psig.toByteArray(), pubnonce.toByteArray(), pubkey.value.toByteArray(), cache.data.toByteArray(), toByteArray()) == 1 + } + + /** + * @param psigs partial signatures + * @return the aggregate of all input partial signatures + */ + public fun add(psigs: List): ByteVector64 { + return Secp256k1.musigPartialSigAgg(toByteArray(), psigs.map { it.toByteArray() }.toTypedArray()).byteVector64() + } + + public companion object { + /** + * @param aggregatedNonce aggregated public nonce + * @param msg message to sign + * @param cache key aggregation cache + * @return a Musig signing session + */ + @JvmStatic + public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Session { + val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), msg.toByteArray(), cache.data.toByteArray()) + return Session(session.byteVector()) + } + } +} + +/** + * 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() == Secp256k1.MUSIG2_SECRET_NONCE_SIZE) { "musig2 secret nonce must be ${Secp256k1.MUSIG2_SECRET_NONCE_SIZE} bytes" } + } + + public companion object { + /** + * @param sessionId random session id. Must not be reused !! + * @param seckey optional private key + * @param pubkey public key + * @param msg optional message to sign + * @param cache optional key aggregation cache + * @param extraInput optional extra input value + * @return a (secret nonce, public nonce) tuple + */ + @JvmStatic + public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Pair { + val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), seckey?.value?.toByteArray(), pubkey.value.toByteArray(), msg?.toByteArray(), cache?.data?.toByteArray(), extraInput?.toByteArray()) + return Pair(SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)), IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE))) + } + } +} + +/** + * 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() == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) { "individual musig2 public nonce must be ${Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + public companion object { + @JvmStatic + public fun aggregate(nonces: List): AggregatedNonce { + val agg = Secp256k1.musigNonceAgg(nonces.map { it.toByteArray() }.toTypedArray()) + return AggregatedNonce(agg) + } + } +} + +/** + * Musig2 aggregated nonce + */ +public data class AggregatedNonce(val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + + public constructor(hex: String) : this(Hex.decode(hex)) + + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) { "aggregated musig2 public nonce must be ${Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt new file mode 100644 index 00000000..868efef4 --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt @@ -0,0 +1,288 @@ +package fr.acinq.bitcoin.crypto.musig2 + +import fr.acinq.bitcoin.* +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 `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 (aggkey, _) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + assertEquals(expected, aggkey) + } + 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 (_, cache) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + tweakIndices.zip(isXonly).forEach { cache = cache.tweak(tweaks[it.first], it.second).first } + } + } + } + + @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) + if (aggpk == null) { + val (_, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), null, extraInput?.byteVector32()) + // assertEquals(expectedSecnonce, secnonce) + assertEquals(expectedPubnonce, pubnonce) + } + } + } + + @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 `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 = ByteVector32.fromValidHex(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 cache = run { + var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> + c = c.tweak(tweak, isXonly).first + } + c + } + val session = Session.build(aggnonce, msg, cache) + val aggsig = session.add(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 cache = run { + var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> + c = c.tweak(tweak, isXonly).first + } + c + } + val session = Session.build(aggnonce, msg, cache) + assertFails { + session.add(psigIndices.map { psigs[it] }) + } + } + } + + @Test + fun `simple musig2 example`() { + val 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 nonces = privkeys.map { + SecretNonce.generate(random.nextBytes(32).byteVector32(), it, it.publicKey(), null, null, null) + } + val secnonces = nonces.map { it.first } + val pubnonces = nonces.map { it.second } + + // aggregate public nonces + val aggnonce = IndividualNonce.aggregate(pubnonces) + val cache = run { + val (_, c) = KeyAggCache.add(pubkeys, null) + val (c1, _) = c.tweak(plainTweak, false) + val (c2, _) = c1.tweak(xonlyTweak, true) + c2 + } + val session = Session.build(aggnonce, msg, cache) + // create partial signatures + val psigs = privkeys.indices.map { + session.sign(secnonces[it], privkeys[it], cache) + } + + // verify partial signatures + pubkeys.indices.forEach { + assertTrue(session.verify(psigs[it], pubnonces[it], pubkeys[it], cache)) + } + + // aggregate partial signatures + session.add(psigs) + } + + // aggregate public keys + val aggpub = run { + val (_, c) = KeyAggCache.add(pubkeys, null) + val (c1, _) = c.tweak(plainTweak, false) + val (_, p) = c1.tweak(xonlyTweak, true) + p + } + + // check that the aggregated signature is a valid, plain Schnorr signature for the aggregated public key + assertTrue(Crypto.verifySignatureSchnorr(msg, aggsig, aggpub.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, cache) = KeyAggCache.add(listOf(alicePubKey, bobPubKey), null) + // 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(random.nextBytes(32).byteVector32(), alicePrivKey, alicePubKey, null, cache, null) + val bobNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), bobPrivKey, bobPubKey, null, null, null) + + val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.second, bobNonce.second)) + 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 (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true) + val session = Session.build(aggnonce, msg, cache1) + val aliceSig = session.sign(aliceNonce.first, alicePrivKey, cache1) + val bobSig = session.sign(bobNonce.first, bobPrivKey, cache1) + session.add(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.Default + + // 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(0, redeemScript) + val merkleRoot = scriptTree.hash() + + // 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, cache) = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey()), null) + + // it is tweaked with the script's merkle root to get the pubkey that will be exposed + val pubkeyScript: List = Script.pay2tr(internalPubKey, merkleRoot) + + 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(random.nextBytes(32).byteVector32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null) + val serverNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null) + + val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + val commonNonce = IndividualNonce.aggregate(listOf(userNonce.second, serverNonce.second)) + + val (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) + val session = Session.build(commonNonce, txHash, cache1) + val userSig = session.sign(userNonce.first, userPrivateKey, cache1) + val serverSig = session.sign(serverNonce.first, serverPrivateKey, cache1) + val commonSig = session.add(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 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 sig = Crypto.signTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, merkleRoot) + val signedTx = tx.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(sig)), scriptTree)) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + } +} \ No newline at end of file From eb10a83a06cdb4706dc2c6fa299d1bffa41d1a75 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 29 Jan 2024 17:22:39 +0100 Subject: [PATCH 3/7] Improve error handling for musig2 module --- .../fr/acinq/bitcoin/crypto/musig2/Musig2.kt | 52 ++++++--- .../crypto/musig2/Musig2TestsCommon.kt | 109 ++++++++++-------- 2 files changed, 95 insertions(+), 66 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt index 8402cb75..e050506c 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt @@ -1,6 +1,7 @@ package fr.acinq.bitcoin.crypto.musig2 import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.utils.Either import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Secp256k1 import kotlin.jvm.JvmStatic @@ -23,14 +24,16 @@ public data class KeyAggCache(val data: ByteVector) { * @param isXonly true if the tweak is an x-only tweak * @return an updated cache, and the tweaked aggregated public key */ - public fun tweak(tweak: ByteVector32, isXonly: Boolean): Pair { + public fun tweak(tweak: ByteVector32, isXonly: Boolean): Either> = try { val localCache = toByteArray() val tweaked = if (isXonly) { Secp256k1.musigPubkeyXonlyTweakAdd(localCache, tweak.toByteArray()) } else { Secp256k1.musigPubkeyTweakAdd(localCache, tweak.toByteArray()) } - return Pair(KeyAggCache(localCache), PublicKey.parse(tweaked)) + Either.Right(Pair(KeyAggCache(localCache), PublicKey.parse(tweaked))) + } catch (t: Throwable) { + Either.Left(t) } public companion object { @@ -40,10 +43,12 @@ public data class KeyAggCache(val data: ByteVector) { * @return a new (if cache was null) or updated cache, and the aggregated public key */ @JvmStatic - public fun add(pubkeys: List, cache: KeyAggCache?): Pair { + public fun add(pubkeys: List, cache: KeyAggCache? = null): Either> = try { val localCache = cache?.data?.toByteArray() ?: ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) val aggkey = Secp256k1.musigPubkeyAgg(pubkeys.map { it.value.toByteArray() }.toTypedArray(), localCache) - return Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector())) + Either.Right(Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector()))) + } catch (t: Throwable) { + Either.Left(t) } } } @@ -64,8 +69,10 @@ public data class Session(val data: ByteVector) { * @param aggCache key aggregation cache * @return a Musig2 partial signature */ - public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): ByteVector32 { - return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32() + public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): Either = try { + Either.Right(Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32()) + } catch (t: Throwable) { + Either.Left(t) } /** @@ -75,18 +82,23 @@ public data class Session(val data: ByteVector) { * @param cache key aggregation cache * @return true if the partial signature is valid */ - public fun verify(psig: ByteVector32, pubnonce: IndividualNonce, pubkey: PublicKey, cache: KeyAggCache): Boolean { - return Secp256k1.musigPartialSigVerify(psig.toByteArray(), pubnonce.toByteArray(), pubkey.value.toByteArray(), cache.data.toByteArray(), toByteArray()) == 1 + public fun verify(psig: ByteVector32, pubnonce: IndividualNonce, pubkey: PublicKey, cache: KeyAggCache): Boolean = try { + Secp256k1.musigPartialSigVerify(psig.toByteArray(), pubnonce.toByteArray(), pubkey.value.toByteArray(), cache.data.toByteArray(), toByteArray()) == 1 + } catch (t: Throwable) { + false } /** * @param psigs partial signatures * @return the aggregate of all input partial signatures */ - public fun add(psigs: List): ByteVector64 { - return Secp256k1.musigPartialSigAgg(toByteArray(), psigs.map { it.toByteArray() }.toTypedArray()).byteVector64() + public fun add(psigs: List): Either = try { + Either.Right(Secp256k1.musigPartialSigAgg(toByteArray(), psigs.map { it.toByteArray() }.toTypedArray()).byteVector64()) + } catch (t: Throwable) { + Either.Left(t) } + public companion object { /** * @param aggregatedNonce aggregated public nonce @@ -95,9 +107,11 @@ public data class Session(val data: ByteVector) { * @return a Musig signing session */ @JvmStatic - public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Session { + public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Either = try { val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), msg.toByteArray(), cache.data.toByteArray()) - return Session(session.byteVector()) + Either.Right(Session(session.byteVector())) + } catch (t: Throwable) { + Either.Left(t) } } } @@ -125,9 +139,13 @@ public data class SecretNonce(val data: ByteVector) { * @return a (secret nonce, public nonce) tuple */ @JvmStatic - public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Pair { + public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Either> = try { val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), seckey?.value?.toByteArray(), pubkey.value.toByteArray(), msg?.toByteArray(), cache?.data?.toByteArray(), extraInput?.toByteArray()) - return Pair(SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)), IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE))) + val secretNonce = SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)) + val publicNonce = IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)) + Either.Right(Pair(secretNonce, publicNonce)) + } catch (t: Throwable) { + Either.Left(t) } } } @@ -148,9 +166,11 @@ public data class IndividualNonce(val data: ByteVector) { public companion object { @JvmStatic - public fun aggregate(nonces: List): AggregatedNonce { + public fun aggregate(nonces: List): Either = try { val agg = Secp256k1.musigNonceAgg(nonces.map { it.toByteArray() }.toTypedArray()) - return AggregatedNonce(agg) + Either.Right(AggregatedNonce(agg)) + } catch (t: Throwable) { + Either.Left(t) } } } diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt index 868efef4..589eb6dd 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.bitcoin.crypto.musig2 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.reference.TransactionTestsCommon +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.secp256k1.Hex import kotlinx.serialization.json.* import kotlin.random.Random @@ -20,7 +21,7 @@ class Musig2TestsCommon { 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 (aggkey, _) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + val (aggkey, _) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! assertEquals(expected, aggkey) } tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { @@ -28,8 +29,8 @@ class Musig2TestsCommon { val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } assertFails { - var (_, cache) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) - tweakIndices.zip(isXonly).forEach { cache = cache.tweak(tweaks[it.first], it.second).first } + var (_, cache) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! + tweakIndices.zip(isXonly).forEach { cache = cache.tweak(tweaks[it.first], it.second).right!!.first } } } } @@ -47,7 +48,7 @@ class Musig2TestsCommon { //val expectedSecnonce = SecretNonce(it.jsonObject["expected_secnonce"]!!.jsonPrimitive.content) val expectedPubnonce = IndividualNonce(it.jsonObject["expected_pubnonce"]!!.jsonPrimitive.content) if (aggpk == null) { - val (_, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), null, extraInput?.byteVector32()) + val (_, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), null, extraInput?.byteVector32()).right!! // assertEquals(expectedSecnonce, secnonce) assertEquals(expectedPubnonce, pubnonce) } @@ -61,13 +62,13 @@ class Musig2TestsCommon { 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] }) + val agg = IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).right!! 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] }) + assertTrue { + IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).isLeft } } } @@ -86,39 +87,39 @@ class Musig2TestsCommon { 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 aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right!! 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 cache = run { - var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> - c = c.tweak(tweak, isXonly).first + c = c.tweak(tweak, isXonly).right!!.first } c } - val session = Session.build(aggnonce, msg, cache) - val aggsig = session.add(psigIndices.map { psigs[it] }) + val session = Session.build(aggnonce, msg, cache).right!! + val aggsig = session.add(psigIndices.map { psigs[it] }).right!! 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 aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right!! 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 cache = run { - var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }, null) + var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> - c = c.tweak(tweak, isXonly).first + c = c.tweak(tweak, isXonly).right!!.first } c } - val session = Session.build(aggnonce, msg, cache) - assertFails { - session.add(psigIndices.map { psigs[it] }) + val session = Session.build(aggnonce, msg, cache).right!! + assertTrue { + session.add(psigIndices.map { psigs[it] }).isLeft } } } @@ -140,23 +141,23 @@ class Musig2TestsCommon { val aggsig = run { val nonces = privkeys.map { - SecretNonce.generate(random.nextBytes(32).byteVector32(), it, it.publicKey(), null, null, null) + SecretNonce.generate(random.nextBytes(32).byteVector32(), it, it.publicKey(), null, null, null).right!! } val secnonces = nonces.map { it.first } val pubnonces = nonces.map { it.second } // aggregate public nonces - val aggnonce = IndividualNonce.aggregate(pubnonces) + val aggnonce = IndividualNonce.aggregate(pubnonces).right!! val cache = run { - val (_, c) = KeyAggCache.add(pubkeys, null) - val (c1, _) = c.tweak(plainTweak, false) - val (c2, _) = c1.tweak(xonlyTweak, true) + val (_, c) = KeyAggCache.add(pubkeys).right!! + val (c1, _) = c.tweak(plainTweak, false).right!! + val (c2, _) = c1.tweak(xonlyTweak, true).right!! c2 } - val session = Session.build(aggnonce, msg, cache) + val session = Session.build(aggnonce, msg, cache).right!! // create partial signatures val psigs = privkeys.indices.map { - session.sign(secnonces[it], privkeys[it], cache) + session.sign(secnonces[it], privkeys[it], cache).right!! } // verify partial signatures @@ -165,14 +166,14 @@ class Musig2TestsCommon { } // aggregate partial signatures - session.add(psigs) + session.add(psigs).right!! } // aggregate public keys val aggpub = run { - val (_, c) = KeyAggCache.add(pubkeys, null) - val (c1, _) = c.tweak(plainTweak, false) - val (_, p) = c1.tweak(xonlyTweak, true) + val (_, c) = KeyAggCache.add(pubkeys).right!! + val (c1, _) = c.tweak(plainTweak, false).right!! + val (_, p) = c1.tweak(xonlyTweak, true).right!! p } @@ -188,7 +189,7 @@ class Musig2TestsCommon { val bobPubKey = bobPrivKey.publicKey() // Alice and Bob exchange public keys and agree on a common aggregated key - val (internalPubKey, cache) = KeyAggCache.add(listOf(alicePubKey, bobPubKey), null) + val (internalPubKey, cache) = KeyAggCache.add(listOf(alicePubKey, bobPubKey)).right!! // we use the standard BIP86 tweak val commonPubKey = internalPubKey.outputKey(Crypto.TaprootTweak.NoScriptTweak).first @@ -199,19 +200,19 @@ class Musig2TestsCommon { 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(random.nextBytes(32).byteVector32(), alicePrivKey, alicePubKey, null, cache, null) - val bobNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), bobPrivKey, bobPubKey, null, null, null) + val random = Random.Default + val aliceNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), alicePrivKey, alicePubKey, null, cache, null).right!! + val bobNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), bobPrivKey, bobPubKey, null, null, null).right!! - val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.second, bobNonce.second)) + val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.second, bobNonce.second)).right!! 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 (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true) - val session = Session.build(aggnonce, msg, cache1) - val aliceSig = session.sign(aliceNonce.first, alicePrivKey, cache1) - val bobSig = session.sign(bobNonce.first, bobPrivKey, cache1) - session.add(listOf(aliceSig, bobSig)) + val (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true).right!! + val session = Session.build(aggnonce, msg, cache1).right!! + val aliceSig = session.sign(aliceNonce.first, alicePrivKey, cache1).right!! + val bobSig = session.sign(bobNonce.first, bobPrivKey, cache1).right!! + session.add(listOf(aliceSig, bobSig)).right!! } // this tx looks like any other tx that spends a p2tr output, with a single signature @@ -235,7 +236,7 @@ class Musig2TestsCommon { val merkleRoot = scriptTree.hash() // 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, cache) = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey()), null) + val (internalPubKey, cache) = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())).right!! // it is tweaked with the script's merkle root to get the pubkey that will be exposed val pubkeyScript: List = Script.pay2tr(internalPubKey, merkleRoot) @@ -257,18 +258,26 @@ class Musig2TestsCommon { ) // 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(random.nextBytes(32).byteVector32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null) - val serverNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null) + val userNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null).right!! + val serverNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null).right!! val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - val commonNonce = IndividualNonce.aggregate(listOf(userNonce.second, serverNonce.second)) - - val (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) - val session = Session.build(commonNonce, txHash, cache1) - val userSig = session.sign(userNonce.first, userPrivateKey, cache1) - val serverSig = session.sign(serverNonce.first, serverPrivateKey, cache1) - val commonSig = session.add(listOf(userSig, serverSig)) - val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig))) + + val commonSig = IndividualNonce.aggregate(listOf(userNonce.second, serverNonce.second)) + .flatMap { commonNonce -> + cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) + .flatMap { (cache1, _) -> + Session.build(commonNonce, txHash, cache1) + .flatMap { session -> + session.sign(userNonce.first, userPrivateKey, cache1) + .flatMap { userSig -> + session.sign(serverNonce.first, serverPrivateKey, cache1) + .flatMap { serverSig -> session.add(listOf(userSig, serverSig)) } + } + } + } + } + val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig.right!!))) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } From bc6e398197b58cd73449d80e53e8f31f7d79375f Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 31 Jan 2024 17:51:52 +0100 Subject: [PATCH 4/7] Don't return Either when not necessary --- .../fr/acinq/bitcoin/crypto/musig2/Musig2.kt | 26 ++++----- .../crypto/musig2/Musig2TestsCommon.kt | 55 +++++++++---------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt index e050506c..4aeb76dd 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Secp256k1 +import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic /** @@ -43,12 +44,11 @@ public data class KeyAggCache(val data: ByteVector) { * @return a new (if cache was null) or updated cache, and the aggregated public key */ @JvmStatic - public fun add(pubkeys: List, cache: KeyAggCache? = null): Either> = try { + @JvmOverloads + public fun add(pubkeys: List, cache: KeyAggCache? = null): Pair { val localCache = cache?.data?.toByteArray() ?: ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) val aggkey = Secp256k1.musigPubkeyAgg(pubkeys.map { it.value.toByteArray() }.toTypedArray(), localCache) - Either.Right(Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector()))) - } catch (t: Throwable) { - Either.Left(t) + return Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector())) } } } @@ -69,10 +69,8 @@ public data class Session(val data: ByteVector) { * @param aggCache key aggregation cache * @return a Musig2 partial signature */ - public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): Either = try { - Either.Right(Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32()) - } catch (t: Throwable) { - Either.Left(t) + public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): ByteVector32 { + return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32() } /** @@ -107,11 +105,9 @@ public data class Session(val data: ByteVector) { * @return a Musig signing session */ @JvmStatic - public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Either = try { + public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Session { val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), msg.toByteArray(), cache.data.toByteArray()) - Either.Right(Session(session.byteVector())) - } catch (t: Throwable) { - Either.Left(t) + return Session(session.byteVector()) } } } @@ -139,13 +135,11 @@ public data class SecretNonce(val data: ByteVector) { * @return a (secret nonce, public nonce) tuple */ @JvmStatic - public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Either> = try { + public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Pair { val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), seckey?.value?.toByteArray(), pubkey.value.toByteArray(), msg?.toByteArray(), cache?.data?.toByteArray(), extraInput?.toByteArray()) val secretNonce = SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)) val publicNonce = IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)) - Either.Right(Pair(secretNonce, publicNonce)) - } catch (t: Throwable) { - Either.Left(t) + return Pair(secretNonce, publicNonce) } } } diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt index 589eb6dd..f93316ef 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt @@ -21,7 +21,7 @@ class Musig2TestsCommon { 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 (aggkey, _) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! + val (aggkey, _) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) assertEquals(expected, aggkey) } tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { @@ -29,7 +29,7 @@ class Musig2TestsCommon { val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } assertFails { - var (_, cache) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! + var (_, cache) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) tweakIndices.zip(isXonly).forEach { cache = cache.tweak(tweaks[it.first], it.second).right!!.first } } } @@ -48,7 +48,7 @@ class Musig2TestsCommon { //val expectedSecnonce = SecretNonce(it.jsonObject["expected_secnonce"]!!.jsonPrimitive.content) val expectedPubnonce = IndividualNonce(it.jsonObject["expected_pubnonce"]!!.jsonPrimitive.content) if (aggpk == null) { - val (_, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), null, extraInput?.byteVector32()).right!! + val (_, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), null, extraInput?.byteVector32()) // assertEquals(expectedSecnonce, secnonce) assertEquals(expectedPubnonce, pubnonce) } @@ -92,13 +92,13 @@ class Musig2TestsCommon { val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) val cache = run { - var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! + var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> c = c.tweak(tweak, isXonly).right!!.first } c } - val session = Session.build(aggnonce, msg, cache).right!! + val session = Session.build(aggnonce, msg, cache) val aggsig = session.add(psigIndices.map { psigs[it] }).right!! assertEquals(expected, aggsig) } @@ -111,13 +111,13 @@ class Musig2TestsCommon { val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) val cache = run { - var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }).right!! + var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> c = c.tweak(tweak, isXonly).right!!.first } c } - val session = Session.build(aggnonce, msg, cache).right!! + val session = Session.build(aggnonce, msg, cache) assertTrue { session.add(psigIndices.map { psigs[it] }).isLeft } @@ -141,7 +141,7 @@ class Musig2TestsCommon { val aggsig = run { val nonces = privkeys.map { - SecretNonce.generate(random.nextBytes(32).byteVector32(), it, it.publicKey(), null, null, null).right!! + SecretNonce.generate(random.nextBytes(32).byteVector32(), it, it.publicKey(), null, null, null) } val secnonces = nonces.map { it.first } val pubnonces = nonces.map { it.second } @@ -149,15 +149,15 @@ class Musig2TestsCommon { // aggregate public nonces val aggnonce = IndividualNonce.aggregate(pubnonces).right!! val cache = run { - val (_, c) = KeyAggCache.add(pubkeys).right!! + val (_, c) = KeyAggCache.add(pubkeys) val (c1, _) = c.tweak(plainTweak, false).right!! val (c2, _) = c1.tweak(xonlyTweak, true).right!! c2 } - val session = Session.build(aggnonce, msg, cache).right!! + val session = Session.build(aggnonce, msg, cache) // create partial signatures val psigs = privkeys.indices.map { - session.sign(secnonces[it], privkeys[it], cache).right!! + session.sign(secnonces[it], privkeys[it], cache) } // verify partial signatures @@ -171,7 +171,7 @@ class Musig2TestsCommon { // aggregate public keys val aggpub = run { - val (_, c) = KeyAggCache.add(pubkeys).right!! + val (_, c) = KeyAggCache.add(pubkeys) val (c1, _) = c.tweak(plainTweak, false).right!! val (_, p) = c1.tweak(xonlyTweak, true).right!! p @@ -189,7 +189,7 @@ class Musig2TestsCommon { val bobPubKey = bobPrivKey.publicKey() // Alice and Bob exchange public keys and agree on a common aggregated key - val (internalPubKey, cache) = KeyAggCache.add(listOf(alicePubKey, bobPubKey)).right!! + val (internalPubKey, cache) = KeyAggCache.add(listOf(alicePubKey, bobPubKey)) // we use the standard BIP86 tweak val commonPubKey = internalPubKey.outputKey(Crypto.TaprootTweak.NoScriptTweak).first @@ -201,17 +201,17 @@ class Musig2TestsCommon { val commonSig = run { val random = Random.Default - val aliceNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), alicePrivKey, alicePubKey, null, cache, null).right!! - val bobNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), bobPrivKey, bobPubKey, null, null, null).right!! + val aliceNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), alicePrivKey, alicePubKey, null, cache, null) + val bobNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), bobPrivKey, bobPubKey, null, null, null) val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.second, bobNonce.second)).right!! 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 (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true).right!! - val session = Session.build(aggnonce, msg, cache1).right!! - val aliceSig = session.sign(aliceNonce.first, alicePrivKey, cache1).right!! - val bobSig = session.sign(bobNonce.first, bobPrivKey, cache1).right!! + val session = Session.build(aggnonce, msg, cache1) + val aliceSig = session.sign(aliceNonce.first, alicePrivKey, cache1) + val bobSig = session.sign(bobNonce.first, bobPrivKey, cache1) session.add(listOf(aliceSig, bobSig)).right!! } @@ -236,7 +236,7 @@ class Musig2TestsCommon { val merkleRoot = scriptTree.hash() // 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, cache) = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())).right!! + val (internalPubKey, cache) = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())) // it is tweaked with the script's merkle root to get the pubkey that will be exposed val pubkeyScript: List = Script.pay2tr(internalPubKey, merkleRoot) @@ -258,8 +258,8 @@ class Musig2TestsCommon { ) // 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(random.nextBytes(32).byteVector32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null).right!! - val serverNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null).right!! + val userNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null) + val serverNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null) val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) @@ -267,16 +267,13 @@ class Musig2TestsCommon { .flatMap { commonNonce -> cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) .flatMap { (cache1, _) -> - Session.build(commonNonce, txHash, cache1) - .flatMap { session -> - session.sign(userNonce.first, userPrivateKey, cache1) - .flatMap { userSig -> - session.sign(serverNonce.first, serverPrivateKey, cache1) - .flatMap { serverSig -> session.add(listOf(userSig, serverSig)) } - } - } + val session = Session.build(commonNonce, txHash, cache1) + val userSig = session.sign(userNonce.first, userPrivateKey, cache1) + val serverSig = session.sign(serverNonce.first, serverPrivateKey, cache1) + session.add(listOf(userSig, serverSig)) } } + val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig.right!!))) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } From 73b455b27f26239c4152ca91d8c69b2af5608b76 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:27:17 +0100 Subject: [PATCH 5/7] Add high-level helpers for using Musig2 with Taproot (#114) * Add high-level helpers for using Musig2 with Taproot When using Musig2 for a taproot key path, we can provide simpler helper functions to collaboratively build a shared signature for the spending transaction. Those helper functions hide the low-level details of using an opaque key aggregation cache or signing session. This comes with a small performance penalty, as we recompute the key aggregation cache. We also document the exposed APIs, import more tests from the official test vectors, and make APIs safe: they should never throw exceptions, except when invalid public keys are provided as inputs (which should be verified by the caller beforehand). * Move taproot signing helpers to `Transaction.kt` This is more consistent with the existing `signInput` helpers. --- .../kotlin/fr/acinq/bitcoin/Crypto.kt | 18 - .../kotlin/fr/acinq/bitcoin/Transaction.kt | 47 +++ .../fr/acinq/bitcoin/crypto/musig2/Musig2.kt | 219 +++++++--- .../fr/acinq/bitcoin/TaprootTestsCommon.kt | 14 +- .../crypto/musig2/Musig2TestsCommon.kt | 367 ++++++++++------- .../fr/acinq/bitcoin/musig2proto/Musig2.kt | 294 ------------- .../bitcoin/musig2proto/Musig2TestsCommon.kt | 389 ------------------ 7 files changed, 444 insertions(+), 904 deletions(-) delete mode 100644 src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt delete mode 100644 src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2TestsCommon.kt diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt index e38ebc98..3b4a6aaf 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt @@ -197,24 +197,6 @@ public object Crypto { return sig } - /** Produce a signature that will be included in the witness of a taproot key path spend. */ - @JvmStatic - public fun signTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List, sighashType: Int, scriptTree: ScriptTree?, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 { - val data = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, inputs, sighashType, annex) - val tweak = when (scriptTree) { - null -> TaprootTweak.NoScriptTweak - else -> TaprootTweak.ScriptTweak(scriptTree.hash()) - } - return signSchnorr(data, privateKey, tweak, auxrand32) - } - - /** Produce a signature that will be included in the witness of a taproot script path spend. */ - @JvmStatic - public fun signTaprootScriptPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List, sighashType: Int, tapleaf: ByteVector32, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 { - val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, inputs, sighashType, tapleaf, annex) - return signSchnorr(data, privateKey, SchnorrTweak.NoTweak, auxrand32) - } - @JvmStatic public fun verifySignatureSchnorr(data: ByteVector32, signature: ByteVector, publicKey: XonlyPublicKey): Boolean { return Secp256k1.verifySchnorr(signature.toByteArray(), data.toByteArray(), publicKey.value.toByteArray()) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt index c16c3f53..3aea069c 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt @@ -798,6 +798,53 @@ public data class Transaction( return hashForSigningSchnorr(tx, inputIndex, inputs, sighashType, SigVersion.SIGVERSION_TAPSCRIPT, tapleaf, annex) } + /** + * Sign a taproot tx input, using the internal key path. + * + * @param privateKey private key. + * @param tx input transaction. + * @param inputIndex index of the tx input that is being signed. + * @param inputs list of all UTXOs spent by this transaction. + * @param sighashType signature hash type, which will be appended to the signature (if not default). + * @param scriptTree tapscript tree of the signed input, if it has script paths. + * @return the schnorr signature of this tx for this specific tx input. + */ + @JvmStatic + public fun signInputTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List, sighashType: Int, scriptTree: ScriptTree?, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 { + val data = hashForSigningTaprootKeyPath(tx, inputIndex, inputs, sighashType, annex) + val tweak = when (scriptTree) { + null -> Crypto.TaprootTweak.NoScriptTweak + else -> Crypto.TaprootTweak.ScriptTweak(scriptTree.hash()) + } + return Crypto.signSchnorr(data, privateKey, tweak, auxrand32) + } + + /** + * Sign a taproot tx input, using one of its script paths. + * + * @param privateKey private key. + * @param tx input transaction. + * @param inputIndex index of the tx input that is being signed. + * @param inputs list of all UTXOs spent by this transaction. + * @param sighashType signature hash type, which will be appended to the signature (if not default). + * @param tapleaf tapscript leaf hash of the script that is being spent. + * @return the schnorr signature of this tx for this specific tx input and the given script leaf. + */ + @JvmStatic + public fun signInputTaprootScriptPath( + privateKey: PrivateKey, + tx: Transaction, + inputIndex: Int, + inputs: List, + sighashType: Int, + tapleaf: ByteVector32, + annex: ByteVector? = null, + auxrand32: ByteVector32? = null + ): ByteVector64 { + val data = hashForSigningTaprootScriptPath(tx, inputIndex, inputs, sighashType, tapleaf, annex) + return Crypto.signSchnorr(data, privateKey, Crypto.SchnorrTweak.NoTweak, auxrand32) + } + @JvmStatic public fun correctlySpends(tx: Transaction, previousOutputs: Map, scriptFlags: Int) { val prevouts = tx.txIn.map { previousOutputs[it.outPoint]!! } diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt index 4aeb76dd..b8cea789 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt @@ -2,16 +2,17 @@ package fr.acinq.bitcoin.crypto.musig2 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Secp256k1 import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic /** - * Musig2 key aggregation cache - * Keeps track of an aggregate of public keys, that can optionally be tweaked + * Musig2 key aggregation cache: keeps track of an aggregate of public keys, that can optionally be tweaked. + * This should be treated as an opaque blob of data, that doesn't contain any sensitive data and thus can be stored. */ -public data class KeyAggCache(val data: ByteVector) { +public data class KeyAggCache(private val data: ByteVector) { public constructor(data: ByteArray) : this(data.byteVector()) init { @@ -20,10 +21,12 @@ public data class KeyAggCache(val data: ByteVector) { public fun toByteArray(): ByteArray = data.toByteArray() + override fun toString(): String = data.toHex() + /** - * @param tweak tweak to apply - * @param isXonly true if the tweak is an x-only tweak - * @return an updated cache, and the tweaked aggregated public key + * @param tweak tweak to apply. + * @param isXonly true if the tweak is an x-only tweak. + * @return an updated cache and the tweaked aggregated public key, or null if one of the tweaks is invalid. */ public fun tweak(tweak: ByteVector32, isXonly: Boolean): Either> = try { val localCache = toByteArray() @@ -39,24 +42,23 @@ public data class KeyAggCache(val data: ByteVector) { public companion object { /** - * @param pubkeys public keys to aggregate - * @param cache an optional key aggregation cache - * @return a new (if cache was null) or updated cache, and the aggregated public key + * @param publicKeys public keys to aggregate: callers must verify that all public keys are valid. + * @return an opaque key aggregation cache and the aggregated public key. */ @JvmStatic - @JvmOverloads - public fun add(pubkeys: List, cache: KeyAggCache? = null): Pair { - val localCache = cache?.data?.toByteArray() ?: ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) - val aggkey = Secp256k1.musigPubkeyAgg(pubkeys.map { it.value.toByteArray() }.toTypedArray(), localCache) + public fun create(publicKeys: List): Pair { + require(publicKeys.all { it.isValid() }) { "some of the public keys provided are not valid" } + val localCache = ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + val aggkey = Secp256k1.musigPubkeyAgg(publicKeys.map { it.value.toByteArray() }.toTypedArray(), localCache) return Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector())) } } } /** - * Musig2 signing session + * Musig2 signing session context that can be used to create partial signatures and aggregate them. */ -public data class Session(val data: ByteVector) { +public data class Session(private val data: ByteVector, private val keyAggCache: KeyAggCache) { init { require(data.size() == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) { "musig2 session must be ${Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE} bytes" } } @@ -64,79 +66,87 @@ public data class Session(val data: ByteVector) { public fun toByteArray(): ByteArray = data.toByteArray() /** - * @param secretNonce secret nonce - * @param pk private key - * @param aggCache key aggregation cache - * @return a Musig2 partial signature + * @param secretNonce signer's secret nonce (see [SecretNonce.generate]). + * @param privateKey signer's private key. + * @return a musig2 partial signature. */ - public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): ByteVector32 { - return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32() + public fun sign(secretNonce: SecretNonce, privateKey: PrivateKey): ByteVector32 { + return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), privateKey.value.toByteArray(), keyAggCache.toByteArray(), this.toByteArray()).byteVector32() } /** - * @param psig musig2 partial signature - * @param pubnonce public nonce, that must match the secret nonce psig was generated with - * @param pubkey public key, that must match the private key psig was generated with - * @param cache key aggregation cache - * @return true if the partial signature is valid + * @param partialSig musig2 partial signature. + * @param publicNonce individual public nonce of the signing participant. + * @param publicKey individual public key of the signing participant. + * @return true if the partial signature is valid. */ - public fun verify(psig: ByteVector32, pubnonce: IndividualNonce, pubkey: PublicKey, cache: KeyAggCache): Boolean = try { - Secp256k1.musigPartialSigVerify(psig.toByteArray(), pubnonce.toByteArray(), pubkey.value.toByteArray(), cache.data.toByteArray(), toByteArray()) == 1 + public fun verify(partialSig: ByteVector32, publicNonce: IndividualNonce, publicKey: PublicKey): Boolean = try { + Secp256k1.musigPartialSigVerify(partialSig.toByteArray(), publicNonce.toByteArray(), publicKey.value.toByteArray(), keyAggCache.toByteArray(), this.toByteArray()) == 1 } catch (t: Throwable) { false } /** - * @param psigs partial signatures - * @return the aggregate of all input partial signatures + * Aggregate partial signatures from all participants into a single schnorr signature. Callers should verify the + * resulting signature, which may be invalid without raising an error here (for example if the set of partial + * signatures is valid but incomplete). + * + * @param partialSigs partial signatures from all signing participants. + * @return the aggregate signature of all input partial signatures or null if a partial signature is invalid. */ - public fun add(psigs: List): Either = try { - Either.Right(Secp256k1.musigPartialSigAgg(toByteArray(), psigs.map { it.toByteArray() }.toTypedArray()).byteVector64()) + public fun aggregateSigs(partialSigs: List): Either = try { + Either.Right(Secp256k1.musigPartialSigAgg(this.toByteArray(), partialSigs.map { it.toByteArray() }.toTypedArray()).byteVector64()) } catch (t: Throwable) { Either.Left(t) } - public companion object { /** - * @param aggregatedNonce aggregated public nonce - * @param msg message to sign - * @param cache key aggregation cache - * @return a Musig signing session + * @param aggregatedNonce aggregated public nonce. + * @param message message that will be signed. + * @param keyAggCache key aggregation cache. + * @return a musig2 signing session. */ @JvmStatic - public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Session { - val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), msg.toByteArray(), cache.data.toByteArray()) - return Session(session.byteVector()) + public fun create(aggregatedNonce: AggregatedNonce, message: ByteVector32, keyAggCache: KeyAggCache): Session { + val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), message.toByteArray(), keyAggCache.toByteArray()) + return Session(session.byteVector(), keyAggCache) } } } /** - * Musig2 secret nonce. Not meant to be reused !! + * Musig2 secret nonce, that should be treated as a private opaque blob. + * This nonce must never be persisted or reused across signing sessions. */ -public data class SecretNonce(val data: ByteVector) { +public data class SecretNonce(internal val data: ByteVector) { public constructor(bin: ByteArray) : this(bin.byteVector()) - public constructor(hex: String) : this(Hex.decode(hex)) init { require(data.size() == Secp256k1.MUSIG2_SECRET_NONCE_SIZE) { "musig2 secret nonce must be ${Secp256k1.MUSIG2_SECRET_NONCE_SIZE} bytes" } } + override fun toString(): String = "" + public companion object { /** - * @param sessionId random session id. Must not be reused !! - * @param seckey optional private key - * @param pubkey public key - * @param msg optional message to sign - * @param cache optional key aggregation cache - * @param extraInput optional extra input value - * @return a (secret nonce, public nonce) tuple + * Generate a secret nonce to be used in a musig2 signing session. + * This nonce must never be persisted or reused across signing sessions. + * All optional arguments exist to enrich the quality of the randomness used, which is critical for security. + * + * @param sessionId unique session ID. + * @param privateKey (optional) signer's private key. + * @param publicKey signer's public key. + * @param message (optional) message that will be signed, if already known. + * @param keyAggCache (optional) key aggregation cache data from the signing session. + * @param extraInput (optional) additional random data. + * @return secret nonce and the corresponding public nonce. */ @JvmStatic - public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Pair { - val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), seckey?.value?.toByteArray(), pubkey.value.toByteArray(), msg?.toByteArray(), cache?.data?.toByteArray(), extraInput?.toByteArray()) + public fun generate(sessionId: ByteVector32, privateKey: PrivateKey?, publicKey: PublicKey, message: ByteVector32?, keyAggCache: KeyAggCache?, extraInput: ByteVector32?): Pair { + privateKey?.let { require(it.publicKey() == publicKey) { "if the private key is provided, it must match the public key" } } + val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), privateKey?.value?.toByteArray(), publicKey.value.toByteArray(), message?.toByteArray(), keyAggCache?.toByteArray(), extraInput?.toByteArray()) val secretNonce = SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)) val publicNonce = IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)) return Pair(secretNonce, publicNonce) @@ -145,11 +155,11 @@ public data class SecretNonce(val data: ByteVector) { } /** - * Musig2 public nonce + * Musig2 public nonce, that must be shared with other participants in the signing session. + * It contains two elliptic curve points, but should be treated as an opaque blob. */ public data class IndividualNonce(val data: ByteVector) { public constructor(bin: ByteArray) : this(bin.byteVector()) - public constructor(hex: String) : this(Hex.decode(hex)) init { @@ -158,7 +168,13 @@ public data class IndividualNonce(val data: ByteVector) { public fun toByteArray(): ByteArray = data.toByteArray() + override fun toString(): String = data.toHex() + public companion object { + /** + * Aggregate public nonces from all participants of a signing session. + * Returns null if one of the nonces provided is invalid. + */ @JvmStatic public fun aggregate(nonces: List): Either = try { val agg = Secp256k1.musigNonceAgg(nonces.map { it.toByteArray() }.toTypedArray()) @@ -170,11 +186,10 @@ public data class IndividualNonce(val data: ByteVector) { } /** - * Musig2 aggregated nonce + * Musig2 aggregate public nonce from all participants of a signing session. */ public data class AggregatedNonce(val data: ByteVector) { public constructor(bin: ByteArray) : this(bin.byteVector()) - public constructor(hex: String) : this(Hex.decode(hex)) init { @@ -182,4 +197,96 @@ public data class AggregatedNonce(val data: ByteVector) { } public fun toByteArray(): ByteArray = data.toByteArray() + + override fun toString(): String = data.toHex() +} + +/** + * This object contain helper functions to use musig2 in the context of spending taproot outputs. + * In order to provide a simpler API, some operations are internally duplicated: if performance is an issue, you should + * consider using the lower-level APIs directly (see [Session] and [KeyAggCache]). + */ +public object Musig2 { + /** + * Aggregate the public keys of a musig2 session into a single public key. + * Note that this function doesn't apply any tweak: when used for taproot, it computes the internal public key, not + * the public key exposed in the script (which is tweaked with the script tree). + * + * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. + */ + public fun aggregateKeys(publicKeys: List): XonlyPublicKey = KeyAggCache.create(publicKeys).first + + /** + * @param sessionId a random, unique session ID. + * @param privateKey signer's private key. + * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. + */ + public fun generateNonce(sessionId: ByteVector32, privateKey: PrivateKey, publicKeys: List): Pair { + val (_, keyAggCache) = KeyAggCache.create(publicKeys) + return SecretNonce.generate(sessionId, privateKey, privateKey.publicKey(), message = null, keyAggCache, extraInput = null) + } + + private fun taprootSession(tx: Transaction, inputIndex: Int, inputs: List, publicKeys: List, publicNonces: List, scriptTree: ScriptTree?): Either { + return IndividualNonce.aggregate(publicNonces).flatMap { aggregateNonce -> + val (aggregatePublicKey, keyAggCache) = KeyAggCache.create(publicKeys) + val tweak = when (scriptTree) { + null -> aggregatePublicKey.tweak(Crypto.TaprootTweak.NoScriptTweak) + else -> aggregatePublicKey.tweak(Crypto.TaprootTweak.ScriptTweak(scriptTree)) + } + keyAggCache.tweak(tweak, isXonly = true).map { tweakedKeyAggCache -> + val txHash = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, inputs, SigHash.SIGHASH_DEFAULT) + Session.create(aggregateNonce, txHash, tweakedKeyAggCache.first) + } + } + } + + /** + * Create a partial musig2 signature for the given taproot input key path. + * + * @param privateKey private key of the signing participant. + * @param tx transaction spending the target taproot input. + * @param inputIndex index of the taproot input to spend. + * @param inputs all inputs of the spending transaction. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param secretNonce secret nonce of the signing participant. + * @param publicNonces public nonces of all participants of the musig2 session. + * @param scriptTree tapscript tree of the taproot input, if it has script paths. + */ + public fun signTaprootInput( + privateKey: PrivateKey, + tx: Transaction, + inputIndex: Int, + inputs: List, + publicKeys: List, + secretNonce: SecretNonce, + publicNonces: List, + scriptTree: ScriptTree? + ): Either { + return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).map { it.sign(secretNonce, privateKey) } + } + + /** + * Aggregate partial musig2 signatures into a valid schnorr signature for the given taproot input key path. + * + * @param partialSigs partial musig2 signatures of all participants of the musig2 session. + * @param tx transaction spending the target taproot input. + * @param inputIndex index of the taproot input to spend. + * @param inputs all inputs of the spending transaction. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param publicNonces public nonces of all participants of the musig2 session. + * @param scriptTree tapscript tree of the taproot input, if it has script paths. + */ + @JvmStatic + public fun aggregateTaprootSignatures( + partialSigs: List, + tx: Transaction, + inputIndex: Int, + inputs: List, + publicKeys: List, + publicNonces: List, + scriptTree: ScriptTree? + ): Either { + return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).flatMap { it.aggregateSigs(partialSigs) } + } + } \ 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 69b35640..390a1aa4 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt @@ -95,7 +95,7 @@ class TaprootTestsCommon { 0 ) val sigHashType = 0 - val sig = Crypto.signTaprootKeyPath(privateKey, tx1, 0, listOf(tx.txOut[1]), sigHashType, scriptTree = null) + val sig = Transaction.signInputTaprootKeyPath(privateKey, tx1, 0, listOf(tx.txOut[1]), sigHashType, scriptTree = null) val tx2 = tx1.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) Transaction.correctlySpends(tx2, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -193,7 +193,7 @@ class TaprootTestsCommon { ) // compute all 3 signatures - val sigs = privs.map { Crypto.signTaprootScriptPath(it, tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) } + val sigs = privs.map { Transaction.signInputTaprootScriptPath(it, tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) } // one signature is not enough val tx = tmp.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubkey, scriptTree, ScriptWitness(listOf(sigs[0], sigs[0], sigs[0])), scriptTree)) @@ -262,7 +262,7 @@ class TaprootTestsCommon { lockTime = 0 ) // We still need to provide the tapscript tree because it is used to tweak the private key. - val sig = Crypto.signTaprootKeyPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree) + val sig = Transaction.signInputTaprootKeyPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree) tmp.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) } @@ -287,7 +287,7 @@ class TaprootTestsCommon { txOut = listOf(TxOut(fundingTx1.txOut[0].amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[0].hash()) + val sig = Transaction.signInputTaprootScriptPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[0].hash()) val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves[0], ScriptWitness(listOf(sig)), scriptTree) tmp.updateWitness(0, witness) } @@ -314,7 +314,7 @@ class TaprootTestsCommon { txOut = listOf(TxOut(fundingTx2.txOut[0].amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(privs[1], tmp, 0, listOf(fundingTx2.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[1].hash()) + val sig = Transaction.signInputTaprootScriptPath(privs[1], tmp, 0, listOf(fundingTx2.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[1].hash()) val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves[1], ScriptWitness(listOf(sig)), scriptTree) tmp.updateWitness(0, witness) } @@ -340,7 +340,7 @@ class TaprootTestsCommon { txOut = listOf(TxOut(fundingTx3.txOut[0].amount - Satoshi(5000), addressToPublicKeyScript(blockchain, "tb1qxy9hhxkw7gt76qrm4yzw4j06gkk4evryh8ayp7").right!!)), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(privs[2], tmp, 0, listOf(fundingTx3.txOut[1]), SigHash.SIGHASH_DEFAULT, leaves[2].hash()) + val sig = Transaction.signInputTaprootScriptPath(privs[2], tmp, 0, listOf(fundingTx3.txOut[1]), SigHash.SIGHASH_DEFAULT, leaves[2].hash()) val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves[2], ScriptWitness(listOf(sig)), scriptTree) tmp.updateWitness(0, witness) } @@ -410,7 +410,7 @@ class TaprootTestsCommon { fun `parse and validate large ordinals transaction`() { val file = resourcesDir().resolve("b5a7e05f28d00e4a791759ad7b6bd6799d856693293ceeaad9b0bb93c8851f7f.bin").openReadableFile() val buffer = ByteArray(file.size) - file.readBytes(buffer, 0, buffer.size) + file.readBytes(buffer, 0, buffer.size) file.close() val tx = Transaction.read(buffer) val parentTx = Transaction.read( diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt index f93316ef..46b7778e 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt @@ -2,14 +2,10 @@ package fr.acinq.bitcoin.crypto.musig2 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.reference.TransactionTestsCommon -import fr.acinq.bitcoin.utils.flatMap 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 +import kotlin.test.* class Musig2TestsCommon { @Test @@ -21,20 +17,45 @@ class Musig2TestsCommon { 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 (aggkey, _) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) + val (aggkey, _) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) assertEquals(expected, aggkey) } 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 tweakIndex = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int }.firstOrNull() val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } - assertFails { - var (_, cache) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) - tweakIndices.zip(isXonly).forEach { cache = cache.tweak(tweaks[it.first], it.second).right!!.first } + when (tweakIndex) { + null -> { + // One of the public keys is invalid, so key aggregation will fail. + // Callers must verify that public keys are valid before aggregating them. + assertFails { + KeyAggCache.create(keyIndices.map { pubkeys[it] }) + } + } + else -> { + // The tweak cannot be applied, it would result in an invalid public key. + val (_, cache) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + assertTrue(cache.tweak(tweaks[tweakIndex], isXonly[0]).isLeft) + } } } } + /** Secret nonces in test vectors use a custom encoding. */ + private fun deserializeSecretNonce(hex: String): SecretNonce { + val serialized = Hex.decode(hex) + require(serialized.size == 97) { "secret nonce from test vector should be serialized using 97 bytes" } + // In test vectors, secret nonces are serialized as: + val compressedPublicKey = PublicKey.parse(serialized.takeLast(33).toByteArray()) + // We expect secret nonces serialized as: + // Where we use a different endianness for the public key coordinates than the test vectors. + val uncompressedPublicKey = compressedPublicKey.toUncompressedBin() + val publicKeyX = uncompressedPublicKey.drop(1).take(32).reversed().toByteArray() + val publicKeyY = uncompressedPublicKey.takeLast(32).reversed().toByteArray() + val magic = Hex.decode("220EDCF1") + return SecretNonce(magic + serialized.take(64) + publicKeyX + publicKeyY) + } + @Test fun `generate secret nonce`() { val tests = TransactionTestsCommon.readData("musig2/nonce_gen_vectors.json") @@ -42,15 +63,22 @@ class Musig2TestsCommon { 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 keyagg = it.jsonObject["aggpk"]?.jsonPrimitive?.contentOrNull?.let { + // The test vectors directly provide an aggregated public key: we must manually create the corresponding + // key aggregation cache to correctly test. + val agg = XonlyPublicKey(ByteVector32.fromValidHex(it)) + val magic = Hex.decode("f4adbbdf") + KeyAggCache(magic + agg.publicKey.toUncompressedBin().drop(1) + ByteArray(129) { 0x00 }) + } 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 expectedSecnonce = deserializeSecretNonce(it.jsonObject["expected_secnonce"]!!.jsonPrimitive.content) val expectedPubnonce = IndividualNonce(it.jsonObject["expected_pubnonce"]!!.jsonPrimitive.content) - if (aggpk == null) { - val (_, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), null, extraInput?.byteVector32()) - // assertEquals(expectedSecnonce, secnonce) + // secp256k1 only supports signing 32-byte messages (when provided), which excludes some of the test vectors. + if (msg == null || msg.size == 32) { + val (secnonce, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), keyagg, extraInput?.byteVector32()) assertEquals(expectedPubnonce, pubnonce) + assertEquals(expectedSecnonce, secnonce) } } } @@ -62,13 +90,58 @@ class Musig2TestsCommon { 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] }).right!! + val agg = IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).right + assertNotNull(agg) assertEquals(expected, agg) } tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - assertTrue { - IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).isLeft + assertTrue(IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).isLeft) + } + } + + @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 { deserializeSecretNonce(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 messageIndex = it.jsonObject["msg_index"]!!.jsonPrimitive.int + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) + assertEquals(aggnonces[it.jsonObject["aggnonce_index"]!!.jsonPrimitive.int], aggnonce) + val keyagg = KeyAggCache.create(keyIndices.map { pubkeys[it] }).second + // We only support signing 32-byte messages. + if (msgs[messageIndex].bytes.size == 32) { + val session = Session.create(aggnonce, ByteVector32(msgs[messageIndex]), keyagg) + assertNotNull(session) + val psig = session.sign(secnonces[keyIndices[signerIndex]], sk) + assertEquals(expected, psig) + assertTrue(session.verify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]])) + } + } + tests.jsonObject["verify_fail_test_cases"]!!.jsonArray.forEach { + val psig = Hex.decode(it.jsonObject["sig"]!!.jsonPrimitive.content).byteVector32() + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + val messageIndex = it.jsonObject["msg_index"]!!.jsonPrimitive.int + if (msgs[messageIndex].bytes.size == 32) { + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) + val (_, keyagg) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + val session = Session.create(aggnonce, ByteVector32(msgs[messageIndex]), keyagg) + assertNotNull(session) + assertFalse(session.verify(psig, pnonces[signerIndex], pubkeys[signerIndex])) } } } @@ -87,48 +160,83 @@ class Musig2TestsCommon { 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] }).right!! + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) 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 cache = run { - var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) - tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> - c = c.tweak(tweak, isXonly).right!!.first - } - c - } - val session = Session.build(aggnonce, msg, cache) - val aggsig = session.add(psigIndices.map { psigs[it] }).right!! + val keyagg = tweakIndices + .zip(isXonly) + .map { tweaks[it.first] to it.second } + .fold(KeyAggCache.create(keyIndices.map { pubkeys[it] }).second) { agg, (tweak, isXonly) -> agg.tweak(tweak, isXonly).right!!.first } + val session = Session.create(aggnonce, msg, keyagg) + val aggsig = session.aggregateSigs(psigIndices.map { psigs[it] }).right 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] }).right!! + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) 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 cache = run { - var (_, c) = KeyAggCache.add(keyIndices.map { pubkeys[it] }) - tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }.forEach { (tweak, isXonly) -> - c = c.tweak(tweak, isXonly).right!!.first - } - c - } - val session = Session.build(aggnonce, msg, cache) - assertTrue { - session.add(psigIndices.map { psigs[it] }).isLeft - } + val keyagg = tweakIndices + .zip(isXonly) + .map { tweaks[it.first] to it.second } + .fold(KeyAggCache.create(keyIndices.map { pubkeys[it] }).second) { agg, (tweak, isXonly) -> agg.tweak(tweak, isXonly).right!!.first } + val session = Session.create(aggnonce, msg, keyagg) + assertTrue(session.aggregateSigs(psigIndices.map { psigs[it] }).isLeft) } } @Test - fun `simple musig2 example`() { - val random = Random.Default - val msg = random.nextBytes(32).byteVector32() + 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 = ByteVector32.fromValidHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) + + val secnonce = deserializeSecretNonce(tests.jsonObject["secnonce"]!!.jsonPrimitive.content) + val aggnonce = AggregatedNonce(tests.jsonObject["aggnonce"]!!.jsonPrimitive.content) + assertEquals(pubkeys[0], sk.publicKey()) + assertEquals(aggnonce, IndividualNonce.aggregate(listOf(pnonces[0], pnonces[1], pnonces[2])).right) + + 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] }).right) + 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 keyagg = tweakIndices.fold(KeyAggCache.create(keyIndices.map { pubkeys[it] }).second) { keyAgg, tweakIdx -> keyAgg.tweak(tweaks[tweakIdx], isXonly[tweakIdx]).right!!.first } + val session = Session.create(aggnonce, msg, keyagg) + assertNotNull(session) + val psig = session.sign(secnonce, sk) + assertEquals(expected, psig) + assertTrue(session.verify(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] }).right) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + assertEquals(1, tweakIndices.size) + val tweak = tweaks[tweakIndices.first()] + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean }.first() + val (_, keyagg) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + assertTrue(keyagg.tweak(tweak, isXonly).isLeft) + } + } + + @Test + fun `simple musig2 example`() { + val msg = Random.Default.nextBytes(32).byteVector32() val privkeys = listOf( PrivateKey(ByteArray(32) { 1 }), PrivateKey(ByteArray(32) { 2 }), @@ -139,45 +247,31 @@ class Musig2TestsCommon { 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 nonces = privkeys.map { - SecretNonce.generate(random.nextBytes(32).byteVector32(), it, it.publicKey(), null, null, null) - } - val secnonces = nonces.map { it.first } - val pubnonces = nonces.map { it.second } - - // aggregate public nonces - val aggnonce = IndividualNonce.aggregate(pubnonces).right!! - val cache = run { - val (_, c) = KeyAggCache.add(pubkeys) - val (c1, _) = c.tweak(plainTweak, false).right!! - val (c2, _) = c1.tweak(xonlyTweak, true).right!! - c2 - } - val session = Session.build(aggnonce, msg, cache) - // create partial signatures - val psigs = privkeys.indices.map { - session.sign(secnonces[it], privkeys[it], cache) - } - - // verify partial signatures - pubkeys.indices.forEach { - assertTrue(session.verify(psigs[it], pubnonces[it], pubkeys[it], cache)) - } - - // aggregate partial signatures - session.add(psigs).right!! - } - - // aggregate public keys - val aggpub = run { - val (_, c) = KeyAggCache.add(pubkeys) + // Aggregate public keys from all participants, and apply tweaks. + val (keyAggCache, aggpub) = run { + val (_, c) = KeyAggCache.create(pubkeys) val (c1, _) = c.tweak(plainTweak, false).right!! - val (_, p) = c1.tweak(xonlyTweak, true).right!! - p + c1.tweak(xonlyTweak, true).right!! } - // check that the aggregated signature is a valid, plain Schnorr signature for the aggregated public key + // Generate secret nonces for each participant. + val nonces = privkeys.map { SecretNonce.generate(Random.Default.nextBytes(32).byteVector32(), it, it.publicKey(), message = null, keyAggCache, extraInput = null) } + val secnonces = nonces.map { it.first } + val pubnonces = nonces.map { it.second } + + // Aggregate public nonces. + val aggnonce = IndividualNonce.aggregate(pubnonces).right + assertNotNull(aggnonce) + + // Create partial signatures from each participant. + val session = Session.create(aggnonce, msg, keyAggCache) + val psigs = privkeys.indices.map { session.sign(secnonces[it], privkeys[it]) } + // Verify individual partial signatures. + pubkeys.indices.forEach { assertTrue(session.verify(psigs[it], pubnonces[it], pubkeys[it])) } + // Aggregate partial signatures into a single signature. + val aggsig = session.aggregateSigs(psigs).right + assertNotNull(aggsig) + // Check that the aggregated signature is a valid, plain Schnorr signature for the aggregated public key. assertTrue(Crypto.verifySignatureSchnorr(msg, aggsig, aggpub.xOnly())) } @@ -188,63 +282,59 @@ class Musig2TestsCommon { 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, cache) = KeyAggCache.add(listOf(alicePubKey, bobPubKey)) - // we use the standard BIP86 tweak + // Alice and Bob exchange public keys and agree on a common aggregated key. + val internalPubKey = Musig2.aggregateKeys(listOf(alicePubKey, bobPubKey)) 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 = Random.Default - val aliceNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), alicePrivKey, alicePubKey, null, cache, null) - val bobNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), bobPrivKey, bobPubKey, null, null, null) - - val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.second, bobNonce.second)).right!! - 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 (cache1, _) = cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true).right!! - val session = Session.build(aggnonce, msg, cache1) - val aliceSig = session.sign(aliceNonce.first, alicePrivKey, cache1) - val bobSig = session.sign(bobNonce.first, bobPrivKey, cache1) - session.add(listOf(aliceSig, bobSig)).right!! - } - - // this tx looks like any other tx that spends a p2tr output, with a single signature - val signedSpendingTx = spendingTx.updateWitness(0, ScriptWitness(listOf(commonSig))) + // This tx sends to a taproot script that doesn't contain any script path. + val tx = Transaction(2, listOf(), listOf(TxOut(10_000.sat(), Script.pay2tr(commonPubKey))), 0) + // This tx spends the previous tx with Alice and Bob's signatures. + val spendingTx = Transaction(2, listOf(TxIn(OutPoint(tx, 0), sequence = 0)), listOf(TxOut(10_000.sat(), Script.pay2wpkh(alicePubKey))), 0) + + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val aliceNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), alicePrivKey, listOf(alicePubKey, bobPubKey)) + val bobNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), bobPrivKey, listOf(alicePubKey, bobPubKey)) + + // Once they have each other's public nonce, they can produce partial signatures. + val publicNonces = listOf(aliceNonce.second, bobNonce.second) + val aliceSig = Musig2.signTaprootInput(alicePrivKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), aliceNonce.first, publicNonces, scriptTree = null).right + assertNotNull(aliceSig) + val bobSig = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), bobNonce.first, publicNonces, scriptTree = null).right + assertNotNull(bobSig) + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val aggregateSig = Musig2.aggregateTaprootSignatures(listOf(aliceSig, bobSig), spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null).right + assertNotNull(aggregateSig) + + // This tx looks like any other tx that spends a p2tr output, with a single signature. + val signedSpendingTx = spendingTx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregateSig)) Transaction.correctlySpends(signedSpendingTx, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @Test fun `swap-in-potentiam example with musig2 and taproot`() { val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) + val userPublicKey = userPrivateKey.publicKey() val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) + val serverPublicKey = serverPrivateKey.publicKey() val userRefundPrivateKey = PrivateKey(ByteArray(32) { 3 }) val refundDelay = 25920 - val random = Random.Default - - // 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) + // The redeem script is just the refund script, 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.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) val scriptTree = ScriptTree.Leaf(0, redeemScript) - val merkleRoot = scriptTree.hash() - - // 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, cache) = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())) - // it is tweaked with the script's merkle root to get the pubkey that will be exposed - val pubkeyScript: List = Script.pay2tr(internalPubKey, merkleRoot) + // 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.aggregateKeys(listOf(userPublicKey, serverPublicKey)) + // It is tweaked with the script's merkle root to get the pubkey that will be exposed. + val pubkeyScript = Script.pay2tr(internalPubKey, scriptTree) val swapInTx = Transaction( version = 2, txIn = listOf(), - txOut = listOf(TxOut(Satoshi(10000), pubkeyScript)), + txOut = listOf(TxOut(10_000.sat(), pubkeyScript)), lockTime = 0 ) @@ -253,28 +343,25 @@ class Musig2TestsCommon { val tx = Transaction( version = 2, txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)), - txOut = listOf(TxOut(Satoshi(10000), Script.pay2wpkh(userPrivateKey.publicKey()))), + txOut = listOf(TxOut(10_000.sat(), Script.pay2wpkh(userPublicKey))), 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(random.nextBytes(32).byteVector32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null) - val serverNonce = SecretNonce.generate(random.nextBytes(32).byteVector32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null) - - val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - - val commonSig = IndividualNonce.aggregate(listOf(userNonce.second, serverNonce.second)) - .flatMap { commonNonce -> - cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) - .flatMap { (cache1, _) -> - val session = Session.build(commonNonce, txHash, cache1) - val userSig = session.sign(userNonce.first, userPrivateKey, cache1) - val serverSig = session.sign(serverNonce.first, serverPrivateKey, cache1) - session.add(listOf(userSig, serverSig)) - } - } - - val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig.right!!))) + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val userNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), userPrivateKey, listOf(userPublicKey, serverPublicKey)) + val serverNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), serverPrivateKey, listOf(userPublicKey, serverPublicKey)) + + // Once they have each other's public nonce, they can produce partial signatures. + val publicNonces = listOf(userNonce.second, serverNonce.second) + val userSig = Musig2.signTaprootInput(userPrivateKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), userNonce.first, publicNonces, scriptTree).right + assertNotNull(userSig) + val serverSig = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), serverNonce.first, publicNonces, scriptTree).right + assertNotNull(serverSig) + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val aggregateSig = Musig2.aggregateTaprootSignatures(listOf(userSig, serverSig), tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), publicNonces, scriptTree).right + assertNotNull(aggregateSig) + val signedTx = tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregateSig)) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -283,10 +370,10 @@ class Musig2TestsCommon { val tx = Transaction( version = 2, txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())), - txOut = listOf(TxOut(Satoshi(10000), Script.pay2wpkh(userPrivateKey.publicKey()))), + txOut = listOf(TxOut(10_000.sat(), Script.pay2wpkh(userPublicKey))), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, merkleRoot) + val sig = Transaction.signInputTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, scriptTree.hash()) val signedTx = tx.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(sig)), scriptTree)) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt deleted file mode 100644 index 47ff39d2..00000000 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2.kt +++ /dev/null @@ -1,294 +0,0 @@ -package fr.acinq.bitcoin.musig2proto - -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/musig2proto/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2TestsCommon.kt deleted file mode 100644 index 3205f9a4..00000000 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/musig2proto/Musig2TestsCommon.kt +++ /dev/null @@ -1,389 +0,0 @@ -package fr.acinq.bitcoin.musig2proto - -import fr.acinq.bitcoin.* -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 = 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 = 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.hashForSigningTaprootKeyPath(spendingTx, 0, listOf(tx.txOut[0]), SigHash.SIGHASH_DEFAULT) - - // 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.Default - - // 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(0, redeemScript) - - // 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() - val pubkeyScript = Script.pay2tr(internalPubKey, scriptTree) - - 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.hashForSigningTaprootKeyPath(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT) - 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(scriptTree)), 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, Script.witnessKeyPathPay2tr(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 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 sig = Crypto.signTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(sig)), scriptTree) - val signedTx = tx.updateWitness(0, witness) - Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - } -} \ No newline at end of file From 0a5cf46667240105050e51f6535c2efa7c92fed0 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:32:28 +0100 Subject: [PATCH 6/7] Make functions JvmStatic (#116) --- src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt index b8cea789..9b99c6af 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt @@ -214,6 +214,7 @@ public object Musig2 { * * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. */ + @JvmStatic public fun aggregateKeys(publicKeys: List): XonlyPublicKey = KeyAggCache.create(publicKeys).first /** @@ -221,6 +222,7 @@ public object Musig2 { * @param privateKey signer's private key. * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. */ + @JvmStatic public fun generateNonce(sessionId: ByteVector32, privateKey: PrivateKey, publicKeys: List): Pair { val (_, keyAggCache) = KeyAggCache.create(publicKeys) return SecretNonce.generate(sessionId, privateKey, privateKey.publicKey(), message = null, keyAggCache, extraInput = null) @@ -252,6 +254,7 @@ public object Musig2 { * @param publicNonces public nonces of all participants of the musig2 session. * @param scriptTree tapscript tree of the taproot input, if it has script paths. */ + @JvmStatic public fun signTaprootInput( privateKey: PrivateKey, tx: Transaction, From d559b05643dd55ace5554c1255cb1ee9e0e2e794 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 14 Feb 2024 15:13:39 +0100 Subject: [PATCH 7/7] Use secp256k1-kmp 0.14.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 46c8eec5..0e1900cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,7 +45,7 @@ kotlin { } sourceSets { - val secp256k1KmpVersion = "0.14.0-MUSIG2-SNAPSHOT" + val secp256k1KmpVersion = "0.14.0" val commonMain by getting { dependencies {