diff --git a/jni/c/src/fr_acinq_secp256k1_Secp256k1CFunctions.c b/jni/c/src/fr_acinq_secp256k1_Secp256k1CFunctions.c index 504d077..cf9053c 100644 --- a/jni/c/src/fr_acinq_secp256k1_Secp256k1CFunctions.c +++ b/jni/c/src/fr_acinq_secp256k1_Secp256k1CFunctions.c @@ -687,7 +687,6 @@ JNIEXPORT jbyteArray JNICALL Java_fr_acinq_secp256k1_Secp256k1CFunctions_secp256 secp256k1_context *ctx = (secp256k1_context *)jctx; jbyte *sig; secp256k1_ecdsa_signature signature; - ; unsigned char der[73]; size_t size; int result = 0; @@ -858,7 +857,7 @@ JNIEXPORT jbyteArray JNICALL Java_fr_acinq_secp256k1_Secp256k1CFunctions_secp256 if (jseckey != NULL) { size = (*penv)->GetArrayLength(penv, jseckey); - CHECKRESULT(size != 32, "invalid session_id size"); + CHECKRESULT(size != 32, "invalid private key size"); copy_bytes_from_java(penv, jseckey, size, seckey); } @@ -1016,7 +1015,6 @@ JNIEXPORT jbyteArray JNICALL Java_fr_acinq_secp256k1_Secp256k1CFunctions_secp256 free_pubkeys(pubkeys, count); CHECKRESULT(!result, "secp256k1_musig_pubkey_agg failed"); - size = 32; jpubkey = (*penv)->NewByteArray(penv, 32); pub = (*penv)->GetByteArrayElements(penv, jpubkey, 0); result = secp256k1_xonly_pubkey_serialize(ctx, (unsigned char *)pub, &combined); @@ -1149,7 +1147,7 @@ JNIEXPORT jbyteArray JNICALL Java_fr_acinq_secp256k1_Secp256k1CFunctions_secp256 CHECKRESULT((*penv)->GetArrayLength(penv, jmsg32) != 32, "invalid message size"); if (jkeyaggcache == NULL) return NULL; - CHECKRESULT((*penv)->GetArrayLength(penv, jkeyaggcache) != fr_acinq_secp256k1_Secp256k1CFunctions_SECP256K1_MUSIG_KEYAGG_CACHE_SIZE, "invalid nonce size"); + CHECKRESULT((*penv)->GetArrayLength(penv, jkeyaggcache) != fr_acinq_secp256k1_Secp256k1CFunctions_SECP256K1_MUSIG_KEYAGG_CACHE_SIZE, "invalid keyagg cache size"); ptr = (*penv)->GetByteArrayElements(penv, jaggnonce, 0); result = secp256k1_musig_aggnonce_parse(ctx, &aggnonce, ptr); diff --git a/jni/src/main/java/fr/acinq/secp256k1/Secp256k1CFunctions.java b/jni/src/main/java/fr/acinq/secp256k1/Secp256k1CFunctions.java index 3a9b0e5..fd49e3e 100644 --- a/jni/src/main/java/fr/acinq/secp256k1/Secp256k1CFunctions.java +++ b/jni/src/main/java/fr/acinq/secp256k1/Secp256k1CFunctions.java @@ -29,12 +29,24 @@ public class Secp256k1CFunctions { public static final int SECP256K1_EC_COMPRESSED = (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION); public static final int SECP256K1_EC_UNCOMPRESSED = (SECP256K1_FLAGS_TYPE_COMPRESSION); + /** + * A musig2 public nonce is simply two elliptic curve points. + */ public static final int SECP256K1_MUSIG_PUBLIC_NONCE_SIZE = 66; + /** + * A musig2 private nonce is basically two scalars, but should be treated as an opaque blob. + */ public static final int SECP256K1_MUSIG_SECRET_NONCE_SIZE = 132; + /** + * When aggregating public keys, we cache information in an opaque blob (must not be interpreted). + */ public static final int SECP256K1_MUSIG_KEYAGG_CACHE_SIZE = 197; + /** + * When creating partial signatures and aggregating them, session data is kept in an opaque blob (must not be interpreted). + */ public static final int SECP256K1_MUSIG_SESSION_SIZE = 133; public static native long secp256k1_context_create(int flags); @@ -42,7 +54,7 @@ public class Secp256k1CFunctions { public static native void secp256k1_context_destroy(long ctx); public static native int secp256k1_ec_seckey_verify(long ctx, byte[] seckey); - + public static native byte[] secp256k1_ec_pubkey_parse(long ctx, byte[] pubkey); public static native byte[] secp256k1_ec_pubkey_create(long ctx, byte[] seckey); @@ -93,5 +105,5 @@ public class Secp256k1CFunctions { public static native int secp256k1_musig_partial_sig_verify(long ctx, byte[] psig, byte[] pubnonce, byte[] pubkey, byte[] keyagg_cache, byte[] session); - public static native byte[] secp256k1_musig_partial_sig_agg(long ctx, byte[] session, byte[][] psigs); + public static native byte[] secp256k1_musig_partial_sig_agg(long ctx, byte[] session, byte[][] psigs); } diff --git a/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt b/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt index 93b133e..20ca735 100644 --- a/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt +++ b/jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt @@ -92,36 +92,36 @@ public object NativeSecp256k1 : Secp256k1 { return Secp256k1CFunctions.secp256k1_schnorrsig_sign(Secp256k1Context.getContext(), data, sec, auxrand32) } - override fun musigNonceGen(session_id32: ByteArray, seckey: ByteArray?, pubkey: ByteArray, msg32: ByteArray?, keyagg_cache: ByteArray?, extra_input32: ByteArray?): ByteArray { - return Secp256k1CFunctions.secp256k1_musig_nonce_gen(Secp256k1Context.getContext(), session_id32, seckey, pubkey, msg32, keyagg_cache, extra_input32) + override fun musigNonceGen(sessionId32: ByteArray, privkey: ByteArray?, aggpubkey: ByteArray, msg32: ByteArray?, keyaggCache: ByteArray?, extraInput32: ByteArray?): ByteArray { + return Secp256k1CFunctions.secp256k1_musig_nonce_gen(Secp256k1Context.getContext(), sessionId32, privkey, aggpubkey, msg32, keyaggCache, extraInput32) } override fun musigNonceAgg(pubnonces: Array): ByteArray { return Secp256k1CFunctions.secp256k1_musig_nonce_agg(Secp256k1Context.getContext(), pubnonces) } - override fun musigPubkeyAgg(pubkeys: Array, keyagg_cache: ByteArray?): ByteArray { - return Secp256k1CFunctions.secp256k1_musig_pubkey_agg(Secp256k1Context.getContext(), pubkeys, keyagg_cache) + override fun musigPubkeyAgg(pubkeys: Array, keyaggCache: ByteArray?): ByteArray { + return Secp256k1CFunctions.secp256k1_musig_pubkey_agg(Secp256k1Context.getContext(), pubkeys, keyaggCache) } - override fun musigPubkeyTweakAdd(keyagg_cache: ByteArray, tweak32: ByteArray): ByteArray { - return Secp256k1CFunctions.secp256k1_musig_pubkey_ec_tweak_add(Secp256k1Context.getContext(), keyagg_cache, tweak32) + override fun musigPubkeyTweakAdd(keyaggCache: ByteArray, tweak32: ByteArray): ByteArray { + return Secp256k1CFunctions.secp256k1_musig_pubkey_ec_tweak_add(Secp256k1Context.getContext(), keyaggCache, tweak32) } - override fun musigPubkeyXonlyTweakAdd(keyagg_cache: ByteArray, tweak32: ByteArray): ByteArray { - return Secp256k1CFunctions.secp256k1_musig_pubkey_xonly_tweak_add(Secp256k1Context.getContext(), keyagg_cache, tweak32) + override fun musigPubkeyXonlyTweakAdd(keyaggCache: ByteArray, tweak32: ByteArray): ByteArray { + return Secp256k1CFunctions.secp256k1_musig_pubkey_xonly_tweak_add(Secp256k1Context.getContext(), keyaggCache, tweak32) } - override fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyagg_cache: ByteArray,): ByteArray { - return Secp256k1CFunctions.secp256k1_musig_nonce_process(Secp256k1Context.getContext(), aggnonce, msg32, keyagg_cache) + override fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray { + return Secp256k1CFunctions.secp256k1_musig_nonce_process(Secp256k1Context.getContext(), aggnonce, msg32, keyaggCache) } - override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyagg_cache: ByteArray, session: ByteArray): ByteArray { - return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyagg_cache, session) + override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray { + return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyaggCache, session) } - override fun musigPartialSigVerify(psig: ByteArray, pubnonce: ByteArray, pubkey: ByteArray, keyagg_cache: ByteArray, session: ByteArray): Int { - return Secp256k1CFunctions.secp256k1_musig_partial_sig_verify(Secp256k1Context.getContext(), psig, pubnonce, pubkey, keyagg_cache, session) + override fun musigPartialSigVerify(psig: ByteArray, pubnonce: ByteArray, pubkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): Int { + return Secp256k1CFunctions.secp256k1_musig_partial_sig_verify(Secp256k1Context.getContext(), psig, pubnonce, pubkey, keyaggCache, session) } override fun musigPartialSigAgg(session: ByteArray, psigs: Array): ByteArray { diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index dc3c0d4..fe7c010 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -55,7 +55,7 @@ public interface Secp256k1 { */ public fun signSchnorr(data: ByteArray, sec: ByteArray, auxrand32: ByteArray?): ByteArray - /** + /** * Convert an ECDSA signature to a normalized lower-S form (bitcoin standardness rule). * Returns the normalized signature and a boolean set to true if the input signature was not normalized. * @@ -149,29 +149,108 @@ public interface Secp256k1 { compressed[0] = if (pubkey.last() % 2 == 0) 2.toByte() else 3.toByte() compressed } + else -> throw Secp256k1Exception("invalid public key") } } - public fun musigNonceGen(session_id32: ByteArray, seckey: ByteArray?, pubkey: ByteArray, msg32: ByteArray?, keyagg_cache: ByteArray?, extra_input32: ByteArray?): ByteArray + /** + * 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 sessionId32 unique 32-byte session ID. + * @param privkey (optional) signer's private key. + * @param aggpubkey aggregated public key of all participants in the signing session. + * @param msg32 (optional) 32-byte message that will be signed, if already known. + * @param keyaggCache (optional) key aggregation cache data from the signing session. + * @param extraInput32 (optional) additional 32-byte random data. + * @return serialized version of the secret nonce and the corresponding public nonce. + */ + public fun musigNonceGen(sessionId32: ByteArray, privkey: ByteArray?, aggpubkey: ByteArray, msg32: ByteArray?, keyaggCache: ByteArray?, extraInput32: ByteArray?): ByteArray + /** + * Aggregate public nonces from all participants of a signing session. + * + * @param pubnonces public nonces (one per participant). + * @return 66-byte aggregate public nonce (two public keys) or throws an exception is a nonce is invalid. + */ public fun musigNonceAgg(pubnonces: Array): ByteArray - public fun musigPubkeyAgg(pubkeys: Array, keyagg_cache: ByteArray?): ByteArray + /** + * Aggregate public keys from all participants of a signing session. + * + * @param pubkeys public keys of all participants in the signing session. + * @param keyaggCache (optional) key aggregation cache data from the signing session. If an empty byte array is + * provided, it will be filled with key aggregation data that can be used for the next steps of the signing process. + * @return 32-byte x-only public key. + */ + public fun musigPubkeyAgg(pubkeys: Array, keyaggCache: ByteArray?): ByteArray - public fun musigPubkeyTweakAdd(keyagg_cache: ByteArray, tweak32: ByteArray): ByteArray + /** + * Tweak the aggregated public key of a signing session. + * + * @param keyaggCache key aggregation cache filled by [musigPubkeyAgg]. + * @param tweak32 private key tweak to apply. + * @return P + tweak32 * G (where P is the aggregated public key from [keyaggCache]). The key aggregation cache will + * be updated with the tweaked public key. + */ + public fun musigPubkeyTweakAdd(keyaggCache: ByteArray, tweak32: ByteArray): ByteArray - public fun musigPubkeyXonlyTweakAdd(keyagg_cache: ByteArray, tweak32: ByteArray): ByteArray + /** + * Tweak the aggregated public key of a signing session, treating it as an x-only public key (e.g. when using taproot). + * + * @param keyaggCache key aggregation cache filled by [musigPubkeyAgg]. + * @param tweak32 private key tweak to apply. + * @return with_even_y(P) + tweak32 * G (where P is the aggregated public key from [keyaggCache]). The key aggregation + * cache will be updated with the tweaked public key. + */ + public fun musigPubkeyXonlyTweakAdd(keyaggCache: ByteArray, tweak32: ByteArray): ByteArray - public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyagg_cache: ByteArray): ByteArray + /** + * Create a signing session context based on the public information from all participants. + * + * @param aggnonce aggregated public nonce (see [musigNonceAgg]). + * @param msg32 32-byte message that will be signed. + * @param keyaggCache aggregated public key cache filled by calling [musigPubkeyAgg] with the public keys of all participants. + * @return signing session context that can be used to create partial signatures and aggregate them. + */ + public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray - public fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyagg_cache: ByteArray, session: ByteArray): ByteArray + /** + * Create a partial signature. + * + * @param secnonce signer's secret nonce (see [musigNonceGen]). + * @param privkey signer's private key. + * @param keyaggCache aggregated public key cache filled by calling [musigPubkeyAgg] with the public keys of all participants. + * @param session signing session context (see [musigNonceProcess]). + * @return 32-byte partial signature. + */ + public fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray - public fun musigPartialSigVerify(psig: ByteArray, pubnonce: ByteArray, pubkey: ByteArray, keyagg_cache: ByteArray, session: ByteArray): Int + /** + * Verify the partial signature from one of the signing session's participants. + * + * @param psig 32-byte partial signature. + * @param pubnonce individual public nonce of the signing participant. + * @param pubkey individual public key of the signing participant. + * @param keyaggCache aggregated public key cache filled by calling [musigPubkeyAgg] with the public keys of all participants. + * @param session signing session context (see [musigNonceProcess]). + * @return result code (1 if the partial signature is valid, 0 otherwise). + */ + public fun musigPartialSigVerify(psig: ByteArray, pubnonce: ByteArray, pubkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): Int + /** + * Aggregate partial signatures from all participants into a single schnorr signature. If some of the partial + * signatures are invalid, this function will return an invalid aggregated signature without raising an error. + * It is recommended to use [musigPartialSigVerify] to verify partial signatures first. + * + * @param session signing session context (see [musigNonceProcess]). + * @param psigs list of 32-byte partial signatures. + * @return 64-byte aggregated schnorr signature. + */ public fun musigPartialSigAgg(session: ByteArray, psigs: Array): ByteArray - /** * Delete the secp256k1 context from dynamic memory. */ diff --git a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt index 16e6f40..48468fe 100644 --- a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt +++ b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt @@ -291,26 +291,26 @@ public object Secp256k1Native : Secp256k1 { } } - override fun musigNonceGen(session_id32: ByteArray, seckey: ByteArray?, pubkey: ByteArray, msg32: ByteArray?, keyagg_cache: ByteArray?, extra_input32: ByteArray?): ByteArray { - require(session_id32.size == 32) - seckey?.let { require(it.size == 32) } + override fun musigNonceGen(sessionId32: ByteArray, privkey: ByteArray?, aggpubkey: ByteArray, msg32: ByteArray?, keyaggCache: ByteArray?, extraInput32: ByteArray?): ByteArray { + require(sessionId32.size == 32) + privkey?.let { require(it.size == 32) } msg32?.let { require(it.size == 32) } - keyagg_cache?.let { require(it.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) } - extra_input32?.let { require(it.size == 32) } + keyaggCache?.let { require(it.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) } + extraInput32?.let { require(it.size == 32) } val nonce = memScoped { - val secret_nonce = alloc() - val public_nonce = alloc() - val nPubkey = allocPublicKey(pubkey) - val nKeyAggCache = keyagg_cache?.let { + val secnonce = alloc() + val pubnonce = alloc() + val nPubkey = allocPublicKey(aggpubkey) + val nKeyAggCache = keyaggCache?.let { val n = alloc() memcpy(n.ptr, toNat(it), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) n } - secp256k1_musig_nonce_gen(ctx, secret_nonce.ptr, public_nonce.ptr, toNat(session_id32), seckey?.let { toNat(it) }, nPubkey.ptr, msg32?.let { toNat(it) },nKeyAggCache?.ptr, extra_input32?.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, public_nonce.ptr).requireSuccess("secp256k1_musig_pubnonce_serialize failed") - secret_nonce.ptr.readBytes(Secp256k1.MUSIG2_SECRET_NONCE_SIZE) + nPubnonce.readBytes(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) } return nonce } @@ -326,58 +326,58 @@ public object Secp256k1Native : Secp256k1 { } } - override fun musigPubkeyAgg(pubkeys: Array, keyagg_cache: ByteArray?): ByteArray { + override fun musigPubkeyAgg(pubkeys: Array, keyaggCache: ByteArray?): ByteArray { require(pubkeys.isNotEmpty()) pubkeys.forEach { require(it.size == 33 || it.size == 65) } - keyagg_cache?.let { require(it.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) } + keyaggCache?.let { require(it.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) } memScoped { val nPubkeys = pubkeys.map { allocPublicKey(it).ptr } val combined = alloc() - val nKeyAggCache = keyagg_cache?.let { + val nKeyAggCache = keyaggCache?.let { val n = alloc() memcpy(n.ptr, toNat(it), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) 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) - keyagg_cache?.let { blob -> nKeyAggCache?.let { memcpy(toNat(blob), it.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) } } + keyaggCache?.let { blob -> nKeyAggCache?.let { memcpy(toNat(blob), it.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) } } return agg } } - override fun musigPubkeyTweakAdd(keyagg_cache: ByteArray, tweak32: ByteArray): ByteArray { - require(keyagg_cache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + override fun musigPubkeyTweakAdd(keyaggCache: ByteArray, tweak32: ByteArray): ByteArray { + require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(tweak32.size == 32) memScoped { val nKeyAggCache = alloc() - memcpy(nKeyAggCache.ptr, toNat(keyagg_cache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(nKeyAggCache.ptr, toNat(keyaggCache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) val nPubkey = alloc() secp256k1_musig_pubkey_ec_tweak_add(ctx, nPubkey.ptr, nKeyAggCache.ptr, toNat(tweak32)).requireSuccess("secp256k1_musig_pubkey_ec_tweak_add() failed") - memcpy(toNat(keyagg_cache), nKeyAggCache.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(toNat(keyaggCache), nKeyAggCache.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) return serializePubkey(nPubkey) } } - override fun musigPubkeyXonlyTweakAdd(keyagg_cache: ByteArray, tweak32: ByteArray): ByteArray { - require(keyagg_cache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + override fun musigPubkeyXonlyTweakAdd(keyaggCache: ByteArray, tweak32: ByteArray): ByteArray { + require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(tweak32.size == 32) memScoped { val nKeyAggCache = alloc() - memcpy(nKeyAggCache.ptr, toNat(keyagg_cache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(nKeyAggCache.ptr, toNat(keyaggCache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) val nPubkey = alloc() secp256k1_musig_pubkey_xonly_tweak_add(ctx, nPubkey.ptr, nKeyAggCache.ptr, toNat(tweak32)).requireSuccess("secp256k1_musig_pubkey_xonly_tweak_add() failed") - memcpy(toNat(keyagg_cache), nKeyAggCache.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(toNat(keyaggCache), nKeyAggCache.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) return serializePubkey(nPubkey) } } - override fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyagg_cache: ByteArray): ByteArray { + override fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray { require(aggnonce.size == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) - require(keyagg_cache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(msg32.size == 32) memScoped { val nKeyAggCache = alloc() - memcpy(nKeyAggCache.ptr, toNat(keyagg_cache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(nKeyAggCache.ptr, toNat(keyaggCache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) val nSession = alloc() val nAggnonce = alloc() secp256k1_musig_aggnonce_parse(ctx, nAggnonce.ptr, toNat(aggnonce)).requireSuccess("secp256k1_musig_aggnonce_parse() failed") @@ -388,10 +388,10 @@ public object Secp256k1Native : Secp256k1 { } } - override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyagg_cache: ByteArray, session: ByteArray): ByteArray { + 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(keyagg_cache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) memScoped { @@ -401,7 +401,7 @@ public object Secp256k1Native : Secp256k1 { secp256k1_keypair_create(ctx, nKeypair.ptr, toNat(privkey)) val nPsig = alloc() val nKeyAggCache = alloc() - memcpy(nKeyAggCache.ptr, toNat(keyagg_cache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(nKeyAggCache.ptr, toNat(keyaggCache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) val nSession = alloc() memcpy(nSession.ptr, toNat(session), Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE.toULong()) secp256k1_musig_partial_sign(ctx, nPsig.ptr, nSecnonce.ptr, nKeypair.ptr, nKeyAggCache.ptr, nSession.ptr).requireSuccess("secp256k1_musig_partial_sign failed") @@ -411,11 +411,11 @@ public object Secp256k1Native : Secp256k1 { } } - override fun musigPartialSigVerify(psig: ByteArray, pubnonce: ByteArray, pubkey: ByteArray, keyagg_cache: ByteArray, session: ByteArray): Int { + override fun musigPartialSigVerify(psig: ByteArray, pubnonce: ByteArray, pubkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): Int { require(psig.size == 32) require(pubnonce.size == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) require(pubkey.size == 33 || pubkey.size == 65) - require(keyagg_cache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) memScoped { @@ -423,7 +423,7 @@ public object Secp256k1Native : Secp256k1 { val nPubnonce = allocPublicNonce(pubnonce) val nPubkey = allocPublicKey(pubkey) val nKeyAggCache = alloc() - memcpy(nKeyAggCache.ptr, toNat(keyagg_cache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) + memcpy(nKeyAggCache.ptr, toNat(keyaggCache), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) val nSession = alloc() memcpy(nSession.ptr, toNat(session), Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE.toULong()) return secp256k1_musig_partial_sig_verify(ctx, nPSig.ptr, nPubnonce.ptr, nPubkey.ptr, nKeyAggCache.ptr, nSession.ptr) diff --git a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt index fc594b5..5cc71f8 100644 --- a/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt +++ b/tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt @@ -354,11 +354,14 @@ class Secp256k1Test { @Test fun testMusig2GenerateNonce() { + val privkey = Hex.decode("0000000000000000000000000000000000000000000000000000000000000003") val pubkey = Hex.decode("02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9") val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F") val nonce = Secp256k1.musigNonceGen(sessionId, null, pubkey, null, null, null) val pubnonce = Hex.encode(nonce.copyOfRange(132, 132 + 66)).uppercase() assertEquals("02C96E7CB1E8AA5DAC64D872947914198F607D90ECDE5200DE52978AD5DED63C000299EC5117C2D29EDEE8A2092587C3909BE694D5CFF0667D6C02EA4059F7CD9786", pubnonce) + assertNotEquals(nonce, Secp256k1.musigNonceGen(sessionId, privkey, pubkey, null, null, null)) + assertNotEquals(nonce, Secp256k1.musigNonceGen(sessionId, null, pubkey, sessionId, null, null)) } @Test @@ -368,6 +371,7 @@ class Secp256k1Test { "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + // The following nonces are invalid. "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831", "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" @@ -399,23 +403,37 @@ class Secp256k1Test { "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" - ).map { Hex.decode(it) } val agg1 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[1], pubkeys[2]), null) assertEquals("90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C", Hex.encode(agg1).uppercase()) - val cache = ByteArray(197) - val agg2 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[1], pubkeys[2]), cache) + + // We provide an empty cache, which will be filled when aggregating public keys. + val keyaggCache = ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + val agg2 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[1], pubkeys[2]), keyaggCache) assertEquals("90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C", Hex.encode(agg2).uppercase()) + assertTrue(keyaggCache.count { it.toInt() != 0 } > 100) // the cache has been filled with key aggregation data + + // We can reuse the key aggregation cache to speed up computation. + val agg3 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[1], pubkeys[2]), keyaggCache) + assertEquals("90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C", Hex.encode(agg3).uppercase()) + + val agg4 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[2], pubkeys[1], pubkeys[0]), null) + assertEquals("6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B", Hex.encode(agg4).uppercase()) - val agg3 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[2], pubkeys[1], pubkeys[0]), null) - assertEquals("6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B", Hex.encode(agg3).uppercase()) + val agg5 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[0], pubkeys[0]), null) + assertEquals("B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935", Hex.encode(agg5).uppercase()) - val agg4 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[0], pubkeys[0]), null) - assertEquals("B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935", Hex.encode(agg4).uppercase()) + val agg6 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[0], pubkeys[1], pubkeys[1]), null) + assertEquals("69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E", Hex.encode(agg6).uppercase()) - val agg5 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[0], pubkeys[1], pubkeys[1]), null) - assertEquals("69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E", Hex.encode(agg5).uppercase()) + // If we provide the key aggregation cache for a different session, it is ignored and overwritten. + val agg7 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[0], pubkeys[1], pubkeys[1]), keyaggCache) + assertEquals("69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E", Hex.encode(agg7).uppercase()) + + // If we provide random data in the key aggregation cache, it is ignored and overwritten. + val agg8 = Secp256k1.musigPubkeyAgg(arrayOf(pubkeys[0], pubkeys[0], pubkeys[1], pubkeys[1]), Random.nextBytes(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE)) + assertEquals("69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E", Hex.encode(agg8).uppercase()) } @Test @@ -425,7 +443,7 @@ class Secp256k1Test { "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", "02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337" ).map { Hex.decode(it) }.toTypedArray() - val cache = ByteArray(197) + val cache = ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) val agg1 = Secp256k1.musigPubkeyAgg(pubkeys, cache) assertEquals("b6d830642403fc82511aca5ff98a5e76fcef0f89bffc1aadbe78ee74cd5a5716", Hex.encode(agg1)) val agg2 = Secp256k1.musigPubkeyTweakAdd(cache, Hex.decode("7468697320636f756c64206265206120424950333220747765616b2e2e2e2e00")) @@ -448,21 +466,28 @@ class Secp256k1Test { val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) } val aggnonce = Secp256k1.musigNonceAgg(pubnonces.toTypedArray()) - val caches = (0 until 2).map { ByteArray(197) } - val aggpubkey = Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), caches[0]) - Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), caches[1]) + 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]) val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303") - val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, caches[it]) } + 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], caches[it], sessions[it]) - val check = Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], caches[it], sessions[it]) - assertEquals(1, check) + 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])) psig } + val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray()) - val check = Secp256k1.verifySchnorr(sig, msg32, aggpubkey) - assertTrue(check) + assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray())) + assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey)) + + val invalidSig1 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(psigs[0], psigs[0])) + assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey)) + val invalidSig2 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(Random.nextBytes(32), Random.nextBytes(32))) + assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey)) } @Test