From e532ad80820f2be3bb851de70ae4246431dd27f4 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 16 Apr 2024 14:19:15 +0200 Subject: [PATCH 1/5] Verify musig2 secret nonces Trying to generate a musig2 partial signature with a secret nonce that was generated with a public key that does not match the siging key's public key will trigger secp256k1's illegal callback (which calls abort()) and crash the application. => Here we verify that the secret nonce matches the siging keys before we call secp256k1_musig_partial_sign(). The verification method is a bit hackish (we extract the public key from the secret nonce blob) because secp256k1 does not export the methods we need to do this cleanly. --- .../fr/acinq/secp256k1/NativeSecp256k1.kt | 1 + .../kotlin/fr/acinq/secp256k1/Secp256k1.kt | 21 ++++++++++ .../fr/acinq/secp256k1/Secp256k1Native.kt | 1 + .../fr/acinq/secp256k1/Secp256k1Test.kt | 41 ++++++++++++------- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt b/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt index 20ca735..ec12b29 100644 --- a/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt +++ b/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt @@ -117,6 +117,7 @@ public object NativeSecp256k1 : Secp256k1 { } override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray { + require(musigNoncevalidate(secnonce, pubkeyCreate(privkey))) return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyaggCache, session) } diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index fe7c010..6c2f0a4 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -217,6 +217,26 @@ public interface Secp256k1 { */ public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray + /** + * Validate a musig2 secret nonce + * @param secretnonce secret nonce + * @param pubkey public key that was passed to the nonce generation method + * @return false if the secret nonce does not match the public key + */ + public fun musigNoncevalidate(secretnonce: ByteArray, pubkey: ByteArray): Boolean { + if (secretnonce.size != MUSIG2_SECRET_NONCE_SIZE) return false + if (pubkey.size != 33 && pubkey.size != 65) return false + val pk = Secp256k1.pubkeyParse(pubkey) + // this is a bit hackish but the secp256k1 library does not export methods to do this cleanly + val x = secretnonce.copyOfRange(68, 68 + 32) + x.reverse() + val y = secretnonce.copyOfRange(68 + 32, 68 + 32 + 32) + y.reverse() + val pkx = pk.copyOfRange(1, 1 + 32) + val pky = pk.copyOfRange(33, 33 + 32) + return x.contentEquals(pkx) && y.contentEquals(pky) + } + /** * Create a partial signature. * @@ -256,6 +276,7 @@ public interface Secp256k1 { */ public fun cleanup() + public companion object : Secp256k1 by getSecpk256k1() { @JvmStatic public fun get(): Secp256k1 = this diff --git a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt index 48468fe..81abef6 100644 --- a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt +++ b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt @@ -393,6 +393,7 @@ public object Secp256k1Native : Secp256k1 { require(privkey.size == 32) require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) + require(musigNoncevalidate(secnonce, pubkeyCreate(privkey))) memScoped { val nSecnonce = alloc() diff --git a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt index 5cc71f8..ffa5474 100644 --- a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt +++ b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt @@ -5,6 +5,14 @@ import kotlin.test.* class Secp256k1Test { + val random = Random.Default + + fun randomBytes(length: Int): ByteArray { + val buffer = ByteArray(length) + random.nextBytes(buffer) + return buffer + } + @Test fun verifyValidPrivateKey() { val priv = Hex.decode("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".lowercase()) @@ -451,16 +459,13 @@ class Secp256k1Test { val agg3 = Secp256k1.musigPubkeyXonlyTweakAdd(cache, Hex.decode("7468697320636f756c64206265206120746170726f6f7420747765616b2e2e00")) assertEquals("04537a081a8d32ff700ca86aaa77a423e9b8d1480938076b645c68ee39d263c93948026928799b2d942cb5851db397015b26b1759de1b9ab2c691ced64a2eef836", Hex.encode(agg3)) } - + @Test fun testMusig2SigningSession() { - val privkeys = listOf( - "0101010101010101010101010101010101010101010101010101010101010101", - "0202020202020202020202020202020202020202020202020202020202020202", - ).map { Hex.decode(it) }.toTypedArray() + val privkeys = listOf(randomBytes(32), randomBytes(32)) val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) } - val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F") + val sessionId = randomBytes(32) val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) } val secnonces = nonces.map { it.copyOfRange(0, 132) } val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) } @@ -471,7 +476,7 @@ class Secp256k1Test { assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1])) assertContentEquals(keyaggCaches[0], keyaggCaches[1]) - val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303") + val msg32 = randomBytes(32) val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) } val psigs = (0 until 2).map { val psig = Secp256k1.musigPartialSign(secnonces[it], privkeys[it], keyaggCaches[it], sessions[it]) @@ -480,6 +485,15 @@ class Secp256k1Test { psig } + // signing fails if the secret nonce does not match the private key's public key + assertFails { + Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0]) + } + + assertFails { + Secp256k1.musigPartialSign(secnonces[0], privkeys[1], keyaggCaches[1], sessions[1]) + } + val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray()) assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray())) assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey)) @@ -523,15 +537,14 @@ class Secp256k1Test { } @Test - fun fuzzEcdsaSignVerify() { - val random = Random.Default - - fun randomBytes(length: Int): ByteArray { - val buffer = ByteArray(length) - random.nextBytes(buffer) - return buffer + fun fuzzMusig2SigningSession() { + repeat(1000) { + testMusig2SigningSession() } + } + @Test + fun fuzzEcdsaSignVerify() { repeat(200) { val priv = randomBytes(32) assertTrue(Secp256k1.secKeyVerify(priv)) From 2732f6772e884bd96a6e60bb2c574868d255a39e Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2024 17:28:04 +0200 Subject: [PATCH 2/5] Address review comments --- .../kotlin/fr/acinq/secp256k1/Secp256k1.kt | 10 ++++----- .../fr/acinq/secp256k1/Secp256k1Native.kt | 21 +++++++++++++------ .../fr/acinq/secp256k1/Secp256k1Test.kt | 20 ++++++++++++++---- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index 6c2f0a4..156a70e 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -218,12 +218,12 @@ public interface Secp256k1 { public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray /** - * Validate a musig2 secret nonce - * @param secretnonce secret nonce - * @param pubkey public key that was passed to the nonce generation method - * @return false if the secret nonce does not match the public key + * Check that a secret nonce was generated with a public key that matches the private key used for signing. + * @param secretnonce secret nonce. + * @param pubkey public key for the private key that will be used, with the secret nonce, to generate a partial signature. + * @return false if the secret nonce does not match the public key. */ - public fun musigNoncevalidate(secretnonce: ByteArray, pubkey: ByteArray): Boolean { + public fun musigNonceValidate(secretnonce: ByteArray, pubkey: ByteArray): Boolean { if (secretnonce.size != MUSIG2_SECRET_NONCE_SIZE) return false if (pubkey.size != 33 && pubkey.size != 65) return false val pk = Secp256k1.pubkeyParse(pubkey) diff --git a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt index 81abef6..50e3360 100644 --- a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt +++ b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt @@ -81,7 +81,7 @@ public object Secp256k1Native : Secp256k1 { return serialized.readBytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) } - private fun DeferScope.toNat(bytes: ByteArray): CPointer { + private fun DeferScope.toNat(bytes: ByteArray): CPointer { val ubytes = bytes.asUByteArray() val pinned = ubytes.pin() this.defer { pinned.unpin() } @@ -112,7 +112,7 @@ public object Secp256k1Native : Secp256k1 { } public override fun signatureNormalize(sig: ByteArray): Pair { - require(sig.size >= 64){ "invalid signature ${Hex.encode(sig)}" } + require(sig.size >= 64) { "invalid signature ${Hex.encode(sig)}" } memScoped { val nSig = allocSignature(sig) val isHighS = secp256k1_ecdsa_signature_normalize(ctx, nSig.ptr, nSig.ptr) @@ -307,7 +307,16 @@ public object Secp256k1Native : Secp256k1 { memcpy(n.ptr, toNat(it), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) n } - secp256k1_musig_nonce_gen(ctx, secnonce.ptr, pubnonce.ptr, toNat(sessionId32), privkey?.let { toNat(it) }, nPubkey.ptr, msg32?.let { toNat(it) },nKeyAggCache?.ptr, extraInput32?.let { toNat(it) }).requireSuccess("secp256k1_musig_nonce_gen() failed") + secp256k1_musig_nonce_gen( + ctx, + secnonce.ptr, + pubnonce.ptr, + toNat(sessionId32), + privkey?.let { toNat(it) }, + nPubkey.ptr, + msg32?.let { toNat(it) }, + nKeyAggCache?.ptr, + extraInput32?.let { toNat(it) }).requireSuccess("secp256k1_musig_nonce_gen() failed") val nPubnonce = allocArray(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) secp256k1_musig_pubnonce_serialize(ctx, nPubnonce, pubnonce.ptr).requireSuccess("secp256k1_musig_pubnonce_serialize failed") secnonce.ptr.readBytes(Secp256k1.MUSIG2_SECRET_NONCE_SIZE) + nPubnonce.readBytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) @@ -339,7 +348,7 @@ public object Secp256k1Native : Secp256k1 { n } secp256k1_musig_pubkey_agg(ctx, combined.ptr, nKeyAggCache?.ptr, nPubkeys.toCValues(), pubkeys.size.convert()).requireSuccess("secp256k1_musig_nonce_agg() failed") - val agg = serializeXonlyPubkey(combined) + val agg = serializeXonlyPubkey(combined) keyaggCache?.let { blob -> nKeyAggCache?.let { memcpy(toNat(blob), it.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) } } return agg } @@ -386,14 +395,14 @@ public object Secp256k1Native : Secp256k1 { memcpy(toNat(session), nSession.ptr, Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE.toULong()) return session } - } + } override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray { require(secnonce.size == Secp256k1.MUSIG2_SECRET_NONCE_SIZE) require(privkey.size == 32) require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) - require(musigNoncevalidate(secnonce, pubkeyCreate(privkey))) + require(musigNonceValidate(secnonce, pubkeyCreate(privkey))) memScoped { val nSecnonce = alloc() diff --git a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt index ffa5474..abdc48d 100644 --- a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt +++ b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt @@ -459,13 +459,13 @@ class Secp256k1Test { val agg3 = Secp256k1.musigPubkeyXonlyTweakAdd(cache, Hex.decode("7468697320636f756c64206265206120746170726f6f7420747765616b2e2e00")) assertEquals("04537a081a8d32ff700ca86aaa77a423e9b8d1480938076b645c68ee39d263c93948026928799b2d942cb5851db397015b26b1759de1b9ab2c691ced64a2eef836", Hex.encode(agg3)) } - + @Test fun testMusig2SigningSession() { val privkeys = listOf(randomBytes(32), randomBytes(32)) - val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) } - val sessionId = randomBytes(32) + val msg32 = randomBytes(32) + val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) } val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) } val secnonces = nonces.map { it.copyOfRange(0, 132) } val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) } @@ -476,7 +476,6 @@ class Secp256k1Test { assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1])) assertContentEquals(keyaggCaches[0], keyaggCaches[1]) - val msg32 = randomBytes(32) val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) } val psigs = (0 until 2).map { val psig = Secp256k1.musigPartialSign(secnonces[it], privkeys[it], keyaggCaches[it], sessions[it]) @@ -485,13 +484,26 @@ class Secp256k1Test { psig } + fun printTestData() { + println("private keys") + privkeys.forEach { println(Hex.encode(it)) } + println("sessionId ${Hex.encode(sessionId)}") + println("msg32 ${Hex.encode(msg32)}") + println("nonces") + nonces.forEach { println(Hex.encode(it)) } + } + // signing fails if the secret nonce does not match the private key's public key assertFails { Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0]) + println("musigPartialSign should have failed !") + printTestData() } assertFails { Secp256k1.musigPartialSign(secnonces[0], privkeys[1], keyaggCaches[1], sessions[1]) + println("musigPartialSign should have failed !") + printTestData() } val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray()) From 34045429e1747c015edc1ae5c665c07ba4c3326f Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2024 17:32:12 +0200 Subject: [PATCH 3/5] Fixup! remove empty line --- src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index 156a70e..4ea9794 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -276,7 +276,6 @@ public interface Secp256k1 { */ public fun cleanup() - public companion object : Secp256k1 by getSecpk256k1() { @JvmStatic public fun get(): Secp256k1 = this From a1ddfdeb6580bcbf252763ef1c9f14189aefdf86 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2024 17:35:05 +0200 Subject: [PATCH 4/5] fixup! Address review comments --- jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt b/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt index ec12b29..bcbe03f 100644 --- a/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt +++ b/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt @@ -117,7 +117,7 @@ public object NativeSecp256k1 : Secp256k1 { } override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray { - require(musigNoncevalidate(secnonce, pubkeyCreate(privkey))) + require(musigNonceValidate(secnonce, pubkeyCreate(privkey))) return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyaggCache, session) } From 384cdc25ebe7206c315340f31a77d3a1111f38ec Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2024 17:57:32 +0200 Subject: [PATCH 5/5] Print musig2 test data whenever a test fails --- .../fr/acinq/secp256k1/Secp256k1Test.kt | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt index abdc48d..c714252 100644 --- a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt +++ b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt @@ -467,53 +467,50 @@ class Secp256k1Test { val msg32 = randomBytes(32) val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) } val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) } + val testData = run { + val builder = StringBuilder() + builder.append("private keys\n") + privkeys.forEach { builder.append(Hex.encode(it)).append("\n") } + builder.append("sessionId ${Hex.encode(sessionId)}\n") + builder.append("msg32 ${Hex.encode(msg32)}\n") + builder.append("nonces\n") + nonces.forEach { builder.append(Hex.encode(it)).append("\n") } + builder.toString() + } val secnonces = nonces.map { it.copyOfRange(0, 132) } val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) } val aggnonce = Secp256k1.musigNonceAgg(pubnonces.toTypedArray()) val keyaggCaches = (0 until 2).map { ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) } val aggpubkey = Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[0]) - assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1])) - assertContentEquals(keyaggCaches[0], keyaggCaches[1]) + assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]), testData) + assertContentEquals(keyaggCaches[0], keyaggCaches[1], testData) val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) } val psigs = (0 until 2).map { val psig = Secp256k1.musigPartialSign(secnonces[it], privkeys[it], keyaggCaches[it], sessions[it]) - assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it])) - assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it])) + assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData) + assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData) psig } - fun printTestData() { - println("private keys") - privkeys.forEach { println(Hex.encode(it)) } - println("sessionId ${Hex.encode(sessionId)}") - println("msg32 ${Hex.encode(msg32)}") - println("nonces") - nonces.forEach { println(Hex.encode(it)) } - } - // signing fails if the secret nonce does not match the private key's public key - assertFails { + assertFails(testData) { Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0]) - println("musigPartialSign should have failed !") - printTestData() } - assertFails { + assertFails(testData) { Secp256k1.musigPartialSign(secnonces[0], privkeys[1], keyaggCaches[1], sessions[1]) - println("musigPartialSign should have failed !") - printTestData() } val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray()) - assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray())) - assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey)) + assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()), testData) + assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey), testData) val invalidSig1 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(psigs[0], psigs[0])) - assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey)) + assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey), testData) val invalidSig2 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(Random.nextBytes(32), Random.nextBytes(32))) - assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey)) + assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey), testData) } @Test