From 56535d80af862cabe1fafd004a1a90a0833fd3c0 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Jan 2024 11:44:50 +0100 Subject: [PATCH 1/2] Add documentation and standardize arg names Usage of the Musig2 functions isn't intuitive at all, especially with the key aggregation cache and session data. It's important to provide accurate documentation to help users understand how to correctly produce musig2 signatures. We also change argument names to match Kotlin best practices instead of using the same argument names as C functions. --- .../fr_acinq_secp256k1_Secp256k1CFunctions.c | 6 +- .../acinq/secp256k1/Secp256k1CFunctions.java | 16 +++- .../fr/acinq/secp256k1/NativeSecp256k1.kt | 28 +++--- .../kotlin/fr/acinq/secp256k1/Secp256k1.kt | 94 +++++++++++++++++-- .../fr/acinq/secp256k1/Secp256k1Native.kt | 66 ++++++------- .../fr/acinq/secp256k1/Secp256k1Test.kt | 63 +++++++++---- 6 files changed, 192 insertions(+), 81 deletions(-) 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..18389c1 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. * @@ -153,25 +153,101 @@ public interface Secp256k1 { } } - 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]). + */ + 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]). + */ + 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 From 7688c4960c045ab44d8a9f89c41ac0b1798d853d Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Jan 2024 14:43:09 +0100 Subject: [PATCH 2/2] fixup! Add documentation and standardize arg names --- src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index 18389c1..fe7c010 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -149,6 +149,7 @@ public interface Secp256k1 { compressed[0] = if (pubkey.last() % 2 == 0) 2.toByte() else 3.toByte() compressed } + else -> throw Secp256k1Exception("invalid public key") } } @@ -191,7 +192,8 @@ public interface Secp256k1 { * * @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]). + * @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 @@ -200,7 +202,8 @@ public interface Secp256k1 { * * @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]). + * @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