From c0c9acb77e934c478ad1251dddb2b7f5c9badeaa Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Tue, 9 Sep 2025 23:51:40 +0200 Subject: [PATCH 1/7] refactor: Move parsing helpers from musig to eckey The naming of these functions suggested they should be moved to group.h but this didn't seem practical since these functions depended on eckey.h internally which is higher level than group.h. --- src/eckey.h | 3 +++ src/eckey_impl.h | 33 +++++++++++++++++++++++++++ src/modules/musig/session_impl.h | 39 +++----------------------------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/eckey.h b/src/eckey.h index d54d44c997..c62d23f150 100644 --- a/src/eckey.h +++ b/src/eckey.h @@ -17,6 +17,9 @@ static int secp256k1_eckey_pubkey_parse(secp256k1_ge *elem, const unsigned char *pub, size_t size); static int secp256k1_eckey_pubkey_serialize(secp256k1_ge *elem, unsigned char *pub, size_t *size, int compressed); +static void secp256k1_eckey_serialize_ext(unsigned char *out33, secp256k1_ge* ge); +static int secp256k1_eckey_parse_ext(secp256k1_ge* ge, const unsigned char *in33); + static int secp256k1_eckey_privkey_tweak_add(secp256k1_scalar *key, const secp256k1_scalar *tweak); static int secp256k1_eckey_pubkey_tweak_add(secp256k1_ge *key, const secp256k1_scalar *tweak); static int secp256k1_eckey_privkey_tweak_mul(secp256k1_scalar *key, const secp256k1_scalar *tweak); diff --git a/src/eckey_impl.h b/src/eckey_impl.h index a88a5964d8..67816e19a3 100644 --- a/src/eckey_impl.h +++ b/src/eckey_impl.h @@ -55,6 +55,39 @@ static int secp256k1_eckey_pubkey_serialize(secp256k1_ge *elem, unsigned char *p return 1; } +/* Outputs 33 zero bytes if the given group element is the point at infinity, + * otherwise outputs the compressed serialization */ +static void secp256k1_eckey_serialize_ext(unsigned char *out33, secp256k1_ge* ge) { + if (secp256k1_ge_is_infinity(ge)) { + memset(out33, 0, 33); + } else { + int ret; + size_t size = 33; + ret = secp256k1_eckey_pubkey_serialize(ge, out33, &size, 1); +#ifdef VERIFY + /* Serialize must succeed because the point is not at infinity */ + VERIFY_CHECK(ret && size == 33); +#else + (void) ret; +#endif + } +} + +/* Outputs the point at infinity if the given byte array is all zero, + * otherwise attempts to parse compressed point serialization */ +static int secp256k1_eckey_parse_ext(secp256k1_ge* ge, const unsigned char *in33) { + unsigned char zeros[33] = { 0 }; + + if (secp256k1_memcmp_var(in33, zeros, sizeof(zeros)) == 0) { + secp256k1_ge_set_infinity(ge); + return 1; + } + if (!secp256k1_eckey_pubkey_parse(ge, in33, 33)) { + return 0; + } + return secp256k1_ge_is_in_correct_subgroup(ge); +} + static int secp256k1_eckey_privkey_tweak_add(secp256k1_scalar *key, const secp256k1_scalar *tweak) { secp256k1_scalar_add(key, key, tweak); return !secp256k1_scalar_is_zero(key); diff --git a/src/modules/musig/session_impl.h b/src/modules/musig/session_impl.h index 2c8778b3c0..a2403f9ebd 100644 --- a/src/modules/musig/session_impl.h +++ b/src/modules/musig/session_impl.h @@ -19,39 +19,6 @@ #include "../../scalar.h" #include "../../util.h" -/* Outputs 33 zero bytes if the given group element is the point at infinity and - * otherwise outputs the compressed serialization */ -static void secp256k1_musig_ge_serialize_ext(unsigned char *out33, secp256k1_ge* ge) { - if (secp256k1_ge_is_infinity(ge)) { - memset(out33, 0, 33); - } else { - int ret; - size_t size = 33; - ret = secp256k1_eckey_pubkey_serialize(ge, out33, &size, 1); -#ifdef VERIFY - /* Serialize must succeed because the point is not at infinity */ - VERIFY_CHECK(ret && size == 33); -#else - (void) ret; -#endif - } -} - -/* Outputs the point at infinity if the given byte array is all zero, otherwise - * attempts to parse compressed point serialization. */ -static int secp256k1_musig_ge_parse_ext(secp256k1_ge* ge, const unsigned char *in33) { - unsigned char zeros[33] = { 0 }; - - if (secp256k1_memcmp_var(in33, zeros, sizeof(zeros)) == 0) { - secp256k1_ge_set_infinity(ge); - return 1; - } - if (!secp256k1_eckey_pubkey_parse(ge, in33, 33)) { - return 0; - } - return secp256k1_ge_is_in_correct_subgroup(ge); -} - static const unsigned char secp256k1_musig_secnonce_magic[4] = { 0x22, 0x0e, 0xdc, 0xf1 }; static void secp256k1_musig_secnonce_save(secp256k1_musig_secnonce *secnonce, const secp256k1_scalar *k, const secp256k1_ge *pk) { @@ -246,7 +213,7 @@ int secp256k1_musig_aggnonce_parse(const secp256k1_context* ctx, secp256k1_musig ARG_CHECK(in66 != NULL); for (i = 0; i < 2; i++) { - if (!secp256k1_musig_ge_parse_ext(&ges[i], &in66[33*i])) { + if (!secp256k1_eckey_parse_ext(&ges[i], &in66[33*i])) { return 0; } } @@ -267,7 +234,7 @@ int secp256k1_musig_aggnonce_serialize(const secp256k1_context* ctx, unsigned ch return 0; } for (i = 0; i < 2; i++) { - secp256k1_musig_ge_serialize_ext(&out66[33*i], &ges[i]); + secp256k1_eckey_serialize_ext(&out66[33*i], &ges[i]); } return 1; } @@ -580,7 +547,7 @@ static void secp256k1_musig_compute_noncehash(unsigned char *noncehash, secp256k secp256k1_musig_compute_noncehash_sha256_tagged(&sha); for (i = 0; i < 2; i++) { - secp256k1_musig_ge_serialize_ext(buf, &aggnonce[i]); + secp256k1_eckey_serialize_ext(buf, &aggnonce[i]); secp256k1_sha256_write(&sha, buf, sizeof(buf)); } secp256k1_sha256_write(&sha, agg_pk32, 32); From ec9e8e7a99696cf187f62c0a720566fa01f5537f Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 12 Sep 2025 17:55:17 +0200 Subject: [PATCH 2/7] fullagg: Add module --- .../schnorrsig_fullagg/Makefile.am.include | 3 + src/modules/schnorrsig_fullagg/main_impl.h | 1124 +++++++++++++++++ src/modules/schnorrsig_fullagg/tests_impl.h | 505 ++++++++ src/secp256k1.c | 4 + src/tests.c | 8 + 5 files changed, 1644 insertions(+) create mode 100644 src/modules/schnorrsig_fullagg/Makefile.am.include create mode 100644 src/modules/schnorrsig_fullagg/main_impl.h create mode 100644 src/modules/schnorrsig_fullagg/tests_impl.h diff --git a/src/modules/schnorrsig_fullagg/Makefile.am.include b/src/modules/schnorrsig_fullagg/Makefile.am.include new file mode 100644 index 0000000000..13f21dd370 --- /dev/null +++ b/src/modules/schnorrsig_fullagg/Makefile.am.include @@ -0,0 +1,3 @@ +include_HEADERS += include/secp256k1_schnorrsig_fullagg.h +noinst_HEADERS += src/modules/schnorrsig_fullagg/main_impl.h +noinst_HEADERS += src/modules/schnorrsig_fullagg/tests_impl.h diff --git a/src/modules/schnorrsig_fullagg/main_impl.h b/src/modules/schnorrsig_fullagg/main_impl.h new file mode 100644 index 0000000000..a21dd2684f --- /dev/null +++ b/src/modules/schnorrsig_fullagg/main_impl.h @@ -0,0 +1,1124 @@ +/*********************************************************************** + * Copyright (c) 2025 Fabian Jahr * + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_SCHNORRSIG_FULLAGG_MAIN_H +#define SECP256K1_MODULE_SCHNORRSIG_FULLAGG_MAIN_H + +#include + +#include "../../../include/secp256k1.h" +#include "../../../include/secp256k1_extrakeys.h" +#include "../../../include/secp256k1_schnorrsig_fullagg.h" + +#include "../../eckey.h" +#include "../../group.h" +#include "../../hash.h" +#include "../../scalar.h" +#include "../../util.h" + +static const unsigned char secp256k1_fullagg_secnonce_magic[4] = { 0xf1, 0x1a, 0x99, 0x01 }; + +/* TODO: Share with MuSig */ +static void secp256k1_fullagg_secnonce_save(secp256k1_fullagg_secnonce *secnonce, const secp256k1_scalar *k, const secp256k1_ge *pk) { + memcpy(&secnonce->data[0], secp256k1_fullagg_secnonce_magic, 4); + + secp256k1_scalar_get_b32(&secnonce->data[4], &k[0]); + secp256k1_scalar_get_b32(&secnonce->data[36], &k[1]); + secp256k1_ge_to_bytes(&secnonce->data[68], pk); +} + +/* TODO: Share with MuSig */ +static int secp256k1_fullagg_secnonce_load(const secp256k1_context* ctx, secp256k1_scalar *k, secp256k1_ge *pk, const secp256k1_fullagg_secnonce *secnonce) { + int is_zero; + + ARG_CHECK(secp256k1_memcmp_var(&secnonce->data[0], secp256k1_fullagg_secnonce_magic, 4) == 0); + /* We make very sure that the nonce isn't invalidated by checking the values + * in addition to the magic. */ + is_zero = secp256k1_is_zero_array(&secnonce->data[4], 2 * 32); + secp256k1_declassify(ctx, &is_zero, sizeof(is_zero)); + ARG_CHECK(!is_zero); + + secp256k1_scalar_set_b32(&k[0], &secnonce->data[4], NULL); + secp256k1_scalar_set_b32(&k[1], &secnonce->data[36], NULL); + secp256k1_ge_from_bytes(pk, &secnonce->data[68]); + return 1; +} + +/* TODO: Share with MuSig */ +/* If flag is true, invalidate the secnonce; otherwise leave it. Constant-time. */ +static void secp256k1_fullagg_secnonce_invalidate(const secp256k1_context* ctx, secp256k1_fullagg_secnonce *secnonce, int flag) { + secp256k1_memczero(secnonce->data, sizeof(secnonce->data), flag); + /* The flag argument is usually classified. So, the line above makes the + * magic and public key classified. However, we need both to be + * declassified. Note that we don't declassify the entire object, because if + * flag is 0, then k[0] and k[1] have not been zeroed. */ + secp256k1_declassify(ctx, secnonce->data, sizeof(secp256k1_fullagg_secnonce_magic)); + secp256k1_declassify(ctx, &secnonce->data[68], 64); +} + +static const unsigned char secp256k1_fullagg_pubnonce_magic[4] = { 0xf1, 0x1a, 0x99, 0x02 }; + +/* TODO: Share with MuSig */ +/* Saves two group elements into a pubnonce. */ +static void secp256k1_fullagg_pubnonce_save(secp256k1_fullagg_pubnonce* nonce, const secp256k1_ge* ges) { + int i; + + memcpy(&nonce->data[0], secp256k1_fullagg_pubnonce_magic, 4); + for (i = 0; i < 2; i++) { + secp256k1_ge_to_bytes(nonce->data + 4 + 64*i, &ges[i]); + } +} + +/* TODO: Share with MuSig */ +/* Loads two group elements from a pubnonce. */ +static int secp256k1_fullagg_pubnonce_load(const secp256k1_context* ctx, secp256k1_ge* ges, const secp256k1_fullagg_pubnonce* nonce) { + int i; + + ARG_CHECK(secp256k1_memcmp_var(&nonce->data[0], secp256k1_fullagg_pubnonce_magic, 4) == 0); + for (i = 0; i < 2; i++) { + secp256k1_ge_from_bytes(&ges[i], nonce->data + 4 + 64*i); + } + return 1; +} + +static const unsigned char secp256k1_fullagg_aggnonce_magic[4] = { 0xf1, 0x1a, 0x99, 0x03 }; + +/* TODO: Share with MuSig */ +/* Save aggregate nonce */ +static void secp256k1_fullagg_aggnonce_save(secp256k1_fullagg_aggnonce* nonce, const secp256k1_ge* ges) { + int i; + + memcpy(&nonce->data[0], secp256k1_fullagg_aggnonce_magic, 4); + for (i = 0; i < 2; i++) { + secp256k1_ge_to_bytes_ext(&nonce->data[4 + 64*i], &ges[i]); + } +} + +/* TODO: Share with MuSig */ +/* Load aggregate nonce */ +static int secp256k1_fullagg_aggnonce_load(const secp256k1_context* ctx, secp256k1_ge* ges, const secp256k1_fullagg_aggnonce* nonce) { + int i; + + ARG_CHECK(secp256k1_memcmp_var(&nonce->data[0], secp256k1_fullagg_aggnonce_magic, 4) == 0); + for (i = 0; i < 2; i++) { + secp256k1_ge_from_bytes_ext(&ges[i], &nonce->data[4 + 64*i]); + } + return 1; +} + +static const unsigned char secp256k1_fullagg_partial_sig_magic[4] = { 0xf1, 0x1a, 0x99, 0x05 }; + +/* TODO: Share with MuSig */ +/* Save partial signature */ +static void secp256k1_fullagg_partial_sig_save(secp256k1_fullagg_partial_sig* sig, secp256k1_scalar *s) { + memcpy(&sig->data[0], secp256k1_fullagg_partial_sig_magic, 4); + secp256k1_scalar_get_b32(&sig->data[4], s); +} + +/* TODO: Share with MuSig */ +/* Load partial signature */ +static int secp256k1_fullagg_partial_sig_load(const secp256k1_context* ctx, secp256k1_scalar *s, const secp256k1_fullagg_partial_sig* sig) { + int overflow; + + ARG_CHECK(secp256k1_memcmp_var(&sig->data[0], secp256k1_fullagg_partial_sig_magic, 4) == 0); + secp256k1_scalar_set_b32(s, &sig->data[4], &overflow); + /* Parsed signatures can not overflow */ + VERIFY_CHECK(!overflow); + return 1; +} + +/* TODO: Share with MuSig */ +/* Parse/serialize functions for public interface */ +int secp256k1_fullagg_pubnonce_parse(const secp256k1_context* ctx, secp256k1_fullagg_pubnonce* nonce, const unsigned char *in66) { + secp256k1_ge ges[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(nonce != NULL); + ARG_CHECK(in66 != NULL); + + for (i = 0; i < 2; i++) { + if (!secp256k1_eckey_pubkey_parse(&ges[i], &in66[33*i], 33)) { + return 0; + } + if (!secp256k1_ge_is_in_correct_subgroup(&ges[i])) { + return 0; + } + } + secp256k1_fullagg_pubnonce_save(nonce, ges); + return 1; +} + +/* TODO: Share with MuSig */ +int secp256k1_fullagg_pubnonce_serialize(const secp256k1_context* ctx, unsigned char *out66, const secp256k1_fullagg_pubnonce* nonce) { + secp256k1_ge ges[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out66 != NULL); + ARG_CHECK(nonce != NULL); + memset(out66, 0, 66); + + if (!secp256k1_fullagg_pubnonce_load(ctx, ges, nonce)) { + return 0; + } + for (i = 0; i < 2; i++) { + int ret; + size_t size = 33; + ret = secp256k1_eckey_pubkey_serialize(&ges[i], &out66[33*i], &size, 1); +#ifdef VERIFY + VERIFY_CHECK(ret && size == 33); +#else + (void) ret; +#endif + } + return 1; +} + +/* TODO: Share with MuSig */ +int secp256k1_fullagg_aggnonce_parse(const secp256k1_context* ctx, secp256k1_fullagg_aggnonce* nonce, const unsigned char *in66) { + secp256k1_ge ges[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(nonce != NULL); + ARG_CHECK(in66 != NULL); + + for (i = 0; i < 2; i++) { + if (!secp256k1_eckey_parse_ext(&ges[i], &in66[33*i])) { + return 0; + } + } + secp256k1_fullagg_aggnonce_save(nonce, ges); + return 1; +} + +/* TODO: Share with MuSig */ +int secp256k1_fullagg_aggnonce_serialize(const secp256k1_context* ctx, unsigned char *out66, const secp256k1_fullagg_aggnonce* nonce) { + secp256k1_ge ges[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out66 != NULL); + ARG_CHECK(nonce != NULL); + memset(out66, 0, 66); + + if (!secp256k1_fullagg_aggnonce_load(ctx, ges, nonce)) { + return 0; + } + for (i = 0; i < 2; i++) { + secp256k1_eckey_serialize_ext(&out66[33*i], &ges[i]); + } + return 1; +} + +/* TODO: Share with MuSig */ +int secp256k1_fullagg_partial_sig_parse(const secp256k1_context* ctx, secp256k1_fullagg_partial_sig* sig, const unsigned char *in32) { + secp256k1_scalar tmp; + int overflow; + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(sig != NULL); + ARG_CHECK(in32 != NULL); + + memset(sig, 0, sizeof(*sig)); + + secp256k1_scalar_set_b32(&tmp, in32, &overflow); + if (overflow) { + return 0; + } + secp256k1_fullagg_partial_sig_save(sig, &tmp); + return 1; +} + +/* TODO: Share with MuSig */ +int secp256k1_fullagg_partial_sig_serialize(const secp256k1_context* ctx, unsigned char *out32, const secp256k1_fullagg_partial_sig* sig) { + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out32 != NULL); + ARG_CHECK(sig != NULL); + ARG_CHECK(secp256k1_memcmp_var(&sig->data[0], secp256k1_fullagg_partial_sig_magic, 4) == 0); + + memcpy(out32, &sig->data[4], 32); + return 1; +} + +/* Initializes SHA256 with fixed midstate for "FullAgg/aux" */ +static void secp256k1_fullagg_sha256_tagged_aux(secp256k1_sha256 *sha) { + secp256k1_sha256_initialize(sha); + sha->s[0] = 0x4cb4139bul; + sha->s[1] = 0xac6dd715ul; + sha->s[2] = 0x46eb898bul; + sha->s[3] = 0xc13797e2ul; + sha->s[4] = 0xa7c1aea6ul; + sha->s[5] = 0x21aab077ul; + sha->s[6] = 0x6f1746b2ul; + sha->s[7] = 0x5c2bedd8ul; + sha->bytes = 64; +} + +/* Initializes SHA256 with fixed midstate for "FullAgg/nonce" */ +static void secp256k1_fullagg_sha256_tagged_nonce(secp256k1_sha256 *sha) { + secp256k1_sha256_initialize(sha); + sha->s[0] = 0x742a20a9ul; + sha->s[1] = 0x2c939aaeul; + sha->s[2] = 0xf8f0f6c0ul; + sha->s[3] = 0x9b975422ul; + sha->s[4] = 0xbf5a4f08ul; + sha->s[5] = 0xe5fa99eeul; + sha->s[6] = 0xa64c241ful; + sha->s[7] = 0x5b12ebccul; + sha->bytes = 64; +} + +/* FullAgg nonce generation function */ +static void secp256k1_fullagg_nonce_function(secp256k1_scalar *k, const unsigned char *session_secrand, + const unsigned char *msg32, const unsigned char *seckey32, + const unsigned char *pk33, const unsigned char *extra_input32) { + secp256k1_sha256 sha; + unsigned char rand[32]; + unsigned char i; + + /* Bind nonce to secret key if it was provided, otherwise use secrand directly. */ + if (seckey32 != NULL) { + secp256k1_fullagg_sha256_tagged_aux(&sha); + secp256k1_sha256_write(&sha, session_secrand, 32); + secp256k1_sha256_finalize(&sha, rand); + for (i = 0; i < 32; i++) { + rand[i] ^= seckey32[i]; + } + } else { + memcpy(rand, session_secrand, sizeof(rand)); + } + + /* Write all relevant data into hash for nonce. */ + secp256k1_fullagg_sha256_tagged_nonce(&sha); + secp256k1_sha256_write(&sha, rand, sizeof(rand)); + secp256k1_sha256_write(&sha, pk33, 33); + if (msg32 != NULL) { + secp256k1_sha256_write(&sha, msg32, 32); + } + if (extra_input32 != NULL) { + secp256k1_sha256_write(&sha, extra_input32, 32); + } + + /* Generate the two nonces. */ + for (i = 0; i < 2; i++) { + unsigned char buf[32]; + secp256k1_sha256 sha_tmp = sha; + secp256k1_sha256_write(&sha_tmp, &i, 1); + secp256k1_sha256_finalize(&sha_tmp, buf); + secp256k1_scalar_set_b32(&k[i], buf, NULL); + + secp256k1_memclear_explicit(buf, sizeof(buf)); + secp256k1_sha256_clear(&sha_tmp); + } + secp256k1_memclear_explicit(rand, sizeof(rand)); + secp256k1_sha256_clear(&sha); +} + +/* Internal nonce generation */ +static int secp256k1_fullagg_nonce_gen_internal(const secp256k1_context* ctx, secp256k1_fullagg_secnonce *secnonce, + secp256k1_fullagg_pubnonce *pubnonce, const unsigned char *input_nonce, + const unsigned char *seckey, const secp256k1_pubkey *pubkey, + const unsigned char *msg32, const unsigned char *extra_input32) { + secp256k1_scalar k[2]; + secp256k1_ge nonce_pts[2]; + secp256k1_gej nonce_ptj[2]; + int i; + unsigned char pk_ser[33]; + size_t pk_ser_len = sizeof(pk_ser); + secp256k1_ge pk; + int pk_serialize_success; + int ret = 1; + + ARG_CHECK(pubnonce != NULL); + ARG_CHECK(pubkey != NULL); + ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx)); + memset(pubnonce, 0, sizeof(*pubnonce)); + + /* Check that the seckey is valid to be able to sign for it later. */ + if (seckey != NULL) { + secp256k1_scalar sk; + ret &= secp256k1_scalar_set_b32_seckey(&sk, seckey); + secp256k1_scalar_clear(&sk); + } + + if (!secp256k1_pubkey_load(ctx, &pk, pubkey)) { + return 0; + } + pk_serialize_success = secp256k1_eckey_pubkey_serialize(&pk, pk_ser, &pk_ser_len, 1); + +#ifdef VERIFY + VERIFY_CHECK(pk_serialize_success); + VERIFY_CHECK(pk_ser_len == sizeof(pk_ser)); +#else + (void) pk_serialize_success; +#endif + + /* Get secret nonce */ + secp256k1_fullagg_nonce_function(k, input_nonce, msg32, seckey, pk_ser, extra_input32); + VERIFY_CHECK(!secp256k1_scalar_is_zero(&k[0])); + VERIFY_CHECK(!secp256k1_scalar_is_zero(&k[1])); + secp256k1_fullagg_secnonce_save(secnonce, k, &pk); + secp256k1_fullagg_secnonce_invalidate(ctx, secnonce, !ret); + + /* Compute pubnonce as R1_i = k[0]*G, R2_i = k[1]*G */ + for (i = 0; i < 2; i++) { + secp256k1_ecmult_gen(&ctx->ecmult_gen_ctx, &nonce_ptj[i], &k[i]); + secp256k1_scalar_clear(&k[i]); + } + + /* Convert pubnonce from jacobian to affine and mark as non-secret */ + secp256k1_ge_set_all_gej_var(nonce_pts, nonce_ptj, 2); + for (i = 0; i < 2; i++) { + secp256k1_gej_clear(&nonce_ptj[i]); + secp256k1_declassify(ctx, &nonce_pts[i], sizeof(nonce_pts[i])); + } + + secp256k1_fullagg_pubnonce_save(pubnonce, nonce_pts); + return ret; +} + +int secp256k1_fullagg_nonce_gen(const secp256k1_context* ctx, secp256k1_fullagg_secnonce *secnonce, + secp256k1_fullagg_pubnonce *pubnonce, unsigned char *session_secrand32, + const unsigned char *seckey, const secp256k1_pubkey *pubkey, + const unsigned char *msg32, const unsigned char *extra_input32) { + int ret = 1; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(secnonce != NULL); + ARG_CHECK(session_secrand32 != NULL); + memset(secnonce, 0, sizeof(*secnonce)); + + ret &= !secp256k1_is_zero_array(session_secrand32, 32); + secp256k1_declassify(ctx, &ret, sizeof(ret)); + if (ret == 0) { + secp256k1_fullagg_secnonce_invalidate(ctx, secnonce, 1); + return 0; + } + + ret &= secp256k1_fullagg_nonce_gen_internal(ctx, secnonce, pubnonce, session_secrand32, + seckey, pubkey, msg32, extra_input32); + secp256k1_memczero(session_secrand32, 32, ret); + return ret; +} + +int secp256k1_fullagg_nonce_gen_counter(const secp256k1_context* ctx, secp256k1_fullagg_secnonce *secnonce, + secp256k1_fullagg_pubnonce *pubnonce, uint64_t nonrepeating_cnt, + const secp256k1_keypair *keypair, const unsigned char *msg32, + const unsigned char *extra_input32) { + unsigned char buf[32] = { 0 }; + unsigned char seckey[32]; + secp256k1_pubkey pubkey; + int ret; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(secnonce != NULL); + ARG_CHECK(keypair != NULL); + memset(secnonce, 0, sizeof(*secnonce)); + + secp256k1_write_be64(buf, nonrepeating_cnt); + ret = secp256k1_keypair_sec(ctx, seckey, keypair); + VERIFY_CHECK(ret); + ret = secp256k1_keypair_pub(ctx, &pubkey, keypair); + VERIFY_CHECK(ret); +#ifndef VERIFY + (void) ret; +#endif + + if (!secp256k1_fullagg_nonce_gen_internal(ctx, secnonce, pubnonce, buf, seckey, + &pubkey, msg32, extra_input32)) { + return 0; + } + secp256k1_memclear_explicit(seckey, sizeof(seckey)); + return 1; +} + +static int secp256k1_fullagg_sum_pubnonces(const secp256k1_context* ctx, secp256k1_gej *summed_pubnonces, const secp256k1_fullagg_pubnonce * const* pubnonces, size_t n_pubnonces) { + size_t i; + int j; + + secp256k1_gej_set_infinity(&summed_pubnonces[0]); + secp256k1_gej_set_infinity(&summed_pubnonces[1]); + + for (i = 0; i < n_pubnonces; i++) { + secp256k1_ge nonce_pts[2]; + if (!secp256k1_fullagg_pubnonce_load(ctx, nonce_pts, pubnonces[i])) { + return 0; + } + for (j = 0; j < 2; j++) { + secp256k1_gej_add_ge_var(&summed_pubnonces[j], &summed_pubnonces[j], &nonce_pts[j], NULL); + } + } + return 1; +} + +/* TODO: Share with MuSig */ +/* Aggregate nonces from all signers */ +int secp256k1_fullagg_nonce_agg(const secp256k1_context* ctx, secp256k1_fullagg_aggnonce *aggnonce, + const secp256k1_fullagg_pubnonce * const* pubnonces, size_t n_pubnonces) { + secp256k1_gej aggnonce_ptsj[2]; + secp256k1_ge aggnonce_pts[2]; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(aggnonce != NULL); + ARG_CHECK(pubnonces != NULL); + ARG_CHECK(n_pubnonces > 0); + + if (!secp256k1_fullagg_sum_pubnonces(ctx, aggnonce_ptsj, pubnonces, n_pubnonces)) { + return 0; + } + + secp256k1_ge_set_all_gej_var(aggnonce_pts, aggnonce_ptsj, 2); + secp256k1_fullagg_aggnonce_save(aggnonce, aggnonce_pts); + return 1; +} + +/* Initializes SHA256 with fixed midstate for "FullAgg/noncecoef" */ +static void secp256k1_fullagg_sha256_tagged_noncecoef(secp256k1_sha256 *sha) { + secp256k1_sha256_initialize(sha); + sha->s[0] = 0xe7a1238aul; + sha->s[1] = 0x8d0cb445ul; + sha->s[2] = 0x82ea69faul; + sha->s[3] = 0x6cc8a517ul; + sha->s[4] = 0xfce019a0ul; + sha->s[5] = 0xdc36828ful; + sha->s[6] = 0x727f042bul; + sha->s[7] = 0xf325ff8eul; + sha->bytes = 64; +} + +/* Compute hash_nonce: H_non(R1 || R2 || X_i || m_i || R2_i for all signers) */ +static int secp256k1_fullagg_compute_noncehash(const secp256k1_context* ctx, + unsigned char *noncehash, + const secp256k1_ge *r1, + const secp256k1_ge *r2, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + const secp256k1_fullagg_pubnonce * const *pubnonces, + size_t n_signers) { + unsigned char buf[32]; + size_t i; + secp256k1_sha256 sha; + secp256k1_fullagg_sha256_tagged_noncecoef(&sha); + + if (secp256k1_ge_is_infinity(r1)) { + memset(buf, 0, 32); + } else { + secp256k1_fe_normalize_var((secp256k1_fe*)&r1->x); + secp256k1_fe_get_b32(buf, &r1->x); + } + secp256k1_sha256_write(&sha, buf, 32); + + if (secp256k1_ge_is_infinity(r2)) { + memset(buf, 0, 32); + } else { + secp256k1_fe_normalize_var((secp256k1_fe*)&r2->x); + secp256k1_fe_get_b32(buf, &r2->x); + } + secp256k1_sha256_write(&sha, buf, 32); + + /* Write X_i || m_i || R2_i for all signers */ + for (i = 0; i < n_signers; i++) { + secp256k1_ge pk_ge, nonce_pts[2]; + + /* Load and write X_i */ + if (!secp256k1_pubkey_load(ctx, &pk_ge, pubkeys[i])) { + return 0; + } + secp256k1_fe_normalize_var(&pk_ge.x); + secp256k1_fe_get_b32(buf, &pk_ge.x); + secp256k1_sha256_write(&sha, buf, 32); + + /* Write m_i */ + secp256k1_sha256_write(&sha, messages[i], 32); + + /* Load and write R2_i */ + if (!secp256k1_fullagg_pubnonce_load(ctx, nonce_pts, pubnonces[i])) { + return 0; + } + if (secp256k1_ge_is_infinity(&nonce_pts[1])) { + memset(buf, 0, 32); + } else { + secp256k1_fe_normalize_var(&nonce_pts[1].x); + secp256k1_fe_get_b32(buf, &nonce_pts[1].x); + } + secp256k1_sha256_write(&sha, buf, 32); + } + + secp256k1_sha256_finalize(&sha, noncehash); + return 1; +} + +/* Initializes SHA256 with fixed midstate for "FullAgg/sig" */ +static void secp256k1_fullagg_sha256_tagged_sig(secp256k1_sha256 *sha) { + secp256k1_sha256_initialize(sha); + sha->s[0] = 0xd25dfffbul; + sha->s[1] = 0xd0479fb3ul; + sha->s[2] = 0x32e0d40eul; + sha->s[3] = 0x9c4f065aul; + sha->s[4] = 0xf9bf9e14ul; + sha->s[5] = 0x8f22cce6ul; + sha->s[6] = 0x24f00eaeul; + sha->s[7] = 0xed749b73ul; + sha->bytes = 64; +} + +/* Compute hash_sig: H_sig(L, R, X_i, m_i) where L is list of (X_i, m_i) pairs */ +static int secp256k1_fullagg_compute_sighash(const secp256k1_context* ctx, + secp256k1_scalar *c_i, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + size_t n_signers, + const secp256k1_ge *r, + size_t signer_index) { + unsigned char buf[32]; + unsigned char hash[32]; + size_t i; + secp256k1_ge pk_ge; + secp256k1_sha256 sha; + secp256k1_fullagg_sha256_tagged_sig(&sha); + + /* Write L (list of all X_i || m_i) */ + for (i = 0; i < n_signers; i++) { + if (!secp256k1_pubkey_load(ctx, &pk_ge, pubkeys[i])) { + return 0; + } + secp256k1_fe_normalize_var(&pk_ge.x); + secp256k1_fe_get_b32(buf, &pk_ge.x); + secp256k1_sha256_write(&sha, buf, 32); + secp256k1_sha256_write(&sha, messages[i], 32); + } + + /* Write R */ + secp256k1_fe_normalize_var((secp256k1_fe*)&r->x); + secp256k1_fe_get_b32(buf, &r->x); + secp256k1_sha256_write(&sha, buf, 32); + + /* Write X_i for this signer */ + if (!secp256k1_pubkey_load(ctx, &pk_ge, pubkeys[signer_index])) { + return 0; + } + secp256k1_fe_normalize_var(&pk_ge.x); + secp256k1_fe_get_b32(buf, &pk_ge.x); + secp256k1_sha256_write(&sha, buf, 32); + + /* Write m_i for this signer */ + secp256k1_sha256_write(&sha, messages[signer_index], 32); + + secp256k1_sha256_finalize(&sha, hash); + secp256k1_scalar_set_b32(c_i, hash, NULL); + + return 1; +} + +static const unsigned char secp256k1_fullagg_session_magic[4] = { 0xf1, 0x1a, 0x99, 0x04 }; + +static void secp256k1_fullagg_session_save(secp256k1_fullagg_session *session, + const unsigned char *fin_nonce, + int fin_nonce_parity, + const secp256k1_scalar *noncecoef, + size_t n_signers) { + unsigned char *ptr = session->data; + + memcpy(ptr, secp256k1_fullagg_session_magic, 4); + ptr += 4; + memcpy(ptr, fin_nonce, 32); + ptr += 32; + *ptr = (unsigned char)fin_nonce_parity; + ptr += 1; + secp256k1_scalar_get_b32(ptr, noncecoef); + ptr += 32; + secp256k1_write_be64(ptr, (uint64_t)n_signers); +} + +static int secp256k1_fullagg_session_load(const secp256k1_context* ctx, + unsigned char *fin_nonce, + int *fin_nonce_parity, + secp256k1_scalar *noncecoef, + size_t *n_signers, + const secp256k1_fullagg_session *session) { + const unsigned char *ptr = session->data; + + ARG_CHECK(secp256k1_memcmp_var(ptr, secp256k1_fullagg_session_magic, 4) == 0); + ptr += 4; + memcpy(fin_nonce, ptr, 32); + ptr += 32; + *fin_nonce_parity = (int)*ptr; + ptr += 1; + secp256k1_scalar_set_b32(noncecoef, ptr, NULL); + ptr += 32; + *n_signers = (size_t)secp256k1_read_be64(ptr); + + return 1; +} + + +/* Initialize a FullAgg session */ +int secp256k1_fullagg_session_init(const secp256k1_context* ctx, secp256k1_fullagg_session *session, + const secp256k1_fullagg_aggnonce *aggnonce, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + const secp256k1_fullagg_pubnonce * const *pubnonces, + size_t n_signers) { + secp256k1_ge aggnonce_pts[2]; + secp256k1_ge r; + secp256k1_gej rj; + unsigned char noncehash[32]; + unsigned char fin_nonce[32]; + int fin_nonce_parity; + secp256k1_scalar noncecoef; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(session != NULL); + ARG_CHECK(aggnonce != NULL); + ARG_CHECK(pubkeys != NULL); + ARG_CHECK(messages != NULL); + ARG_CHECK(pubnonces != NULL); + ARG_CHECK(n_signers > 0); + + if (!secp256k1_fullagg_aggnonce_load(ctx, aggnonce_pts, aggnonce)) { + return 0; + } + + /* Compute nonce hash b */ + if (!secp256k1_fullagg_compute_noncehash(ctx, noncehash, &aggnonce_pts[0], &aggnonce_pts[1], + pubkeys, messages, pubnonces, n_signers)) { + return 0; + } + secp256k1_scalar_set_b32(&noncecoef, noncehash, NULL); + + /* Compute effective final nonce R = R1 + b*R2 */ + if (secp256k1_ge_is_infinity(&aggnonce_pts[0]) && secp256k1_ge_is_infinity(&aggnonce_pts[1])) { + /* Both components are infinity, R is infinity */ + secp256k1_ge_set_infinity(&r); + } else if (secp256k1_ge_is_infinity(&aggnonce_pts[0])) { + /* Only R1 is infinity, R = b*R2 */ + secp256k1_gej_set_ge(&rj, &aggnonce_pts[1]); + secp256k1_ecmult(&rj, &rj, &noncecoef, NULL); + secp256k1_ge_set_gej(&r, &rj); + } else if (secp256k1_ge_is_infinity(&aggnonce_pts[1])) { + /* Only R2 is infinity, R = R1 */ + r = aggnonce_pts[0]; + } else { + /* Normal case: R = R1 + b*R2 */ + secp256k1_gej_set_ge(&rj, &aggnonce_pts[1]); + secp256k1_ecmult(&rj, &rj, &noncecoef, NULL); + secp256k1_gej_add_ge_var(&rj, &rj, &aggnonce_pts[0], NULL); + secp256k1_ge_set_gej(&r, &rj); + } + + /* Store final nonce */ + if (secp256k1_ge_is_infinity(&r)) { + /* R is infinity - store zeros for x-coordinate */ + memset(fin_nonce, 0, 32); + fin_nonce_parity = 0; + } else { + /* Normal case - normalize and store x-coordinate */ + secp256k1_fe_normalize_var(&r.x); + secp256k1_fe_normalize_var(&r.y); + secp256k1_fe_get_b32(fin_nonce, &r.x); + fin_nonce_parity = secp256k1_fe_is_odd(&r.y); + } + + secp256k1_fullagg_session_save(session, fin_nonce, fin_nonce_parity, &noncecoef, n_signers); + return 1; +} + +/* TODO: Share with MuSig */ +static void secp256k1_fullagg_partial_sign_clear(secp256k1_scalar *sk, secp256k1_scalar *k) { + secp256k1_scalar_clear(sk); + secp256k1_scalar_clear(&k[0]); + secp256k1_scalar_clear(&k[1]); +} + +/* Create a partial signature */ +int secp256k1_fullagg_partial_sign(const secp256k1_context* ctx, secp256k1_fullagg_partial_sig *partial_sig, + secp256k1_fullagg_secnonce *secnonce, const secp256k1_keypair *keypair, + const secp256k1_fullagg_session *session, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + const secp256k1_fullagg_pubnonce * const *pubnonces, + size_t signer_index) { + secp256k1_scalar sk; + secp256k1_ge pk, keypair_pk; + secp256k1_scalar k[2]; + secp256k1_scalar s, c_i; + unsigned char fin_nonce[32]; + int fin_nonce_parity; + secp256k1_scalar noncecoef; + size_t n_signers; + secp256k1_ge r; + int ret; + int is_fin_nonce_zero; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(secnonce != NULL); + ARG_CHECK(partial_sig != NULL); + ARG_CHECK(keypair != NULL); + ARG_CHECK(session != NULL); + ARG_CHECK(pubkeys != NULL); + ARG_CHECK(messages != NULL); + ARG_CHECK(pubnonces != NULL); + + /* Load and invalidate secnonce */ + ret = secp256k1_fullagg_secnonce_load(ctx, k, &pk, secnonce); + /* Always clear the secnonce to avoid nonce reuse */ + memset(secnonce, 0, sizeof(*secnonce)); + if (!ret) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + if (!secp256k1_keypair_load(ctx, &sk, &keypair_pk, keypair)) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* Verify the keypair matches the secnonce */ + if (!secp256k1_fe_equal(&pk.x, &keypair_pk.x) || !secp256k1_fe_equal(&pk.y, &keypair_pk.y)) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + if (!secp256k1_fullagg_session_load(ctx, fin_nonce, &fin_nonce_parity, &noncecoef, &n_signers, session)) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + if (signer_index >= n_signers) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* Verify this signer's pubkey matches */ + { + secp256k1_ge signer_pk; + if (!secp256k1_pubkey_load(ctx, &signer_pk, pubkeys[signer_index])) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + if (!secp256k1_fe_equal(&pk.x, &signer_pk.x) || !secp256k1_fe_equal(&pk.y, &signer_pk.y)) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + } + + /* Verify this signer's R2 appears exactly once at the correct index */ + { + secp256k1_ge nonce_pts[2]; + secp256k1_gej r2j; + secp256k1_ge r2_self; + int found_count = 0; + int found_index = -1; + size_t j; + + /* Compute our R2 from k[1] */ + secp256k1_ecmult_gen(&ctx->ecmult_gen_ctx, &r2j, &k[1]); + secp256k1_ge_set_gej(&r2_self, &r2j); + + /* Check all pubnonces for our R2 */ + for (j = 0; j < n_signers; j++) { + if (!secp256k1_fullagg_pubnonce_load(ctx, nonce_pts, pubnonces[j])) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* Check if this R2 matches ours (compare both x and y coordinates) */ + if (!secp256k1_ge_is_infinity(&nonce_pts[1]) && !secp256k1_ge_is_infinity(&r2_self)) { + if (secp256k1_fe_equal(&r2_self.x, &nonce_pts[1].x) && + secp256k1_fe_equal(&r2_self.y, &nonce_pts[1].y)) { + found_count++; + found_index = (int)j; + } + } else if (secp256k1_ge_is_infinity(&nonce_pts[1]) && secp256k1_ge_is_infinity(&r2_self)) { + /* Both are infinity */ + found_count++; + found_index = (int)j; + } + } + + /* R2 must appear exactly once */ + if (found_count != 1) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* The index where R2 was found must match our signer_index */ + if (found_index != (int)signer_index) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + } + + /* Check if fin_nonce is zero (infinity case) */ + is_fin_nonce_zero = secp256k1_is_zero_array(fin_nonce, 32); + + if (is_fin_nonce_zero) { + /* R is infinity - cannot sign */ + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* Reconstruct R for computing challenge */ + secp256k1_fe_set_b32_mod(&r.x, fin_nonce); + if (!secp256k1_ge_set_xo_var(&r, &r.x, fin_nonce_parity)) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* Negate nonces if R has odd y */ + if (fin_nonce_parity) { + secp256k1_scalar_negate(&k[0], &k[0]); + secp256k1_scalar_negate(&k[1], &k[1]); + } + + /* Compute signer's challenge c_i */ + if (!secp256k1_fullagg_compute_sighash(ctx, &c_i, pubkeys, messages, n_signers, + &r, signer_index)) { + secp256k1_fullagg_partial_sign_clear(&sk, k); + return 0; + } + + /* Compute s_i = k1_i + b*k2_i + c_i*sk_i */ + secp256k1_scalar_mul(&s, &noncecoef, &k[1]); /* b*k2_i */ + secp256k1_scalar_add(&s, &s, &k[0]); /* + k1_i */ + secp256k1_scalar_mul(&k[0], &c_i, &sk); /* c_i*sk_i (reuse k[0]) */ + secp256k1_scalar_add(&s, &s, &k[0]); /* + c_i*sk_i */ + + secp256k1_fullagg_partial_sig_save(partial_sig, &s); + + /* Clear sensitive data */ + secp256k1_fullagg_partial_sign_clear(&sk, k); + secp256k1_scalar_clear(&s); + secp256k1_scalar_clear(&c_i); + + return 1; +} + +/* TODO: Share with MuSig */ +static void secp256k1_fullagg_effective_nonce(secp256k1_gej *out_nonce, const secp256k1_ge *nonce_pts, const secp256k1_scalar *b) { + secp256k1_gej tmp; + + secp256k1_gej_set_ge(&tmp, &nonce_pts[1]); + secp256k1_ecmult(out_nonce, &tmp, b, NULL); + secp256k1_gej_add_ge_var(out_nonce, out_nonce, &nonce_pts[0], NULL); +} + +/* Verify a partial signature */ +int secp256k1_fullagg_partial_sig_verify(const secp256k1_context* ctx, const secp256k1_fullagg_partial_sig *partial_sig, + const secp256k1_fullagg_pubnonce *pubnonce, const secp256k1_pubkey *pubkey, + const secp256k1_fullagg_session *session, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + size_t signer_index) { + unsigned char fin_nonce[32]; + int fin_nonce_parity; + secp256k1_scalar noncecoef; + size_t n_signers; + secp256k1_scalar s, c_i; + secp256k1_ge pk, r; + secp256k1_ge nonce_pts[2]; + secp256k1_gej rj, pkj, tmp; + int result; + int is_fin_nonce_zero; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(partial_sig != NULL); + ARG_CHECK(pubnonce != NULL); + ARG_CHECK(pubkey != NULL); + ARG_CHECK(session != NULL); + ARG_CHECK(pubkeys != NULL); + ARG_CHECK(messages != NULL); + + if (!secp256k1_fullagg_session_load(ctx, fin_nonce, &fin_nonce_parity, &noncecoef, &n_signers, session)) { + return 0; + } + + if (signer_index >= n_signers) { + return 0; + } + + if (!secp256k1_fullagg_partial_sig_load(ctx, &s, partial_sig)) { + return 0; + } + + if (!secp256k1_fullagg_pubnonce_load(ctx, nonce_pts, pubnonce)) { + return 0; + } + + if (!secp256k1_pubkey_load(ctx, &pk, pubkey)) { + return 0; + } + + if (secp256k1_ge_is_infinity(&pk)) { + return 0; + } + + /* Check if fin_nonce is zero (infinity case) */ + is_fin_nonce_zero = secp256k1_is_zero_array(fin_nonce, 32); + + if (is_fin_nonce_zero) { + return 0; + } + + /* Reconstruct R */ + secp256k1_fe_set_b32_mod(&r.x, fin_nonce); + if (!secp256k1_ge_set_xo_var(&r, &r.x, fin_nonce_parity)) { + return 0; + } + + /* Compute effective nonce: R_eff = R1_i + b*R2_i */ + if (secp256k1_ge_is_infinity(&nonce_pts[0]) && secp256k1_ge_is_infinity(&nonce_pts[1])) { + /* Both nonces are infinity */ + secp256k1_gej_set_infinity(&rj); + } else if (secp256k1_ge_is_infinity(&nonce_pts[0])) { + /* Only R1_i is infinity, R_eff = b*R2_i */ + secp256k1_gej_set_ge(&rj, &nonce_pts[1]); + secp256k1_ecmult(&rj, &rj, &noncecoef, NULL); + } else if (secp256k1_ge_is_infinity(&nonce_pts[1])) { + /* Only R2_i is infinity, R_eff = R1_i */ + secp256k1_gej_set_ge(&rj, &nonce_pts[0]); + } else { + /* Normal case: R_eff = R1_i + b*R2_i */ + secp256k1_fullagg_effective_nonce(&rj, nonce_pts, &noncecoef); + } + + /* Negate if R has odd y */ + if (fin_nonce_parity) { + secp256k1_gej_neg(&rj, &rj); + } + + /* Compute signer's challenge c_i */ + if (!secp256k1_fullagg_compute_sighash(ctx, &c_i, pubkeys, messages, n_signers, + &r, signer_index)) { + return 0; + } + + /* Verify: s_i*G = R_eff + c_i*pk_i */ + secp256k1_scalar_negate(&s, &s); + secp256k1_gej_set_ge(&pkj, &pk); + secp256k1_ecmult(&tmp, &pkj, &c_i, &s); + secp256k1_gej_add_var(&tmp, &tmp, &rj, NULL); + + result = secp256k1_gej_is_infinity(&tmp); + + return result; +} + +/* Aggregate partial signatures */ +int secp256k1_fullagg_partial_sig_agg(const secp256k1_context* ctx, unsigned char *sig64, + const secp256k1_fullagg_session *session, + const secp256k1_fullagg_partial_sig * const *partial_sigs, + size_t n_sigs) { + unsigned char fin_nonce[32]; + int fin_nonce_parity; + secp256k1_scalar noncecoef; + size_t n_signers; + secp256k1_scalar s_agg; + size_t i; + int is_fin_nonce_zero; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(sig64 != NULL); + ARG_CHECK(session != NULL); + ARG_CHECK(partial_sigs != NULL); + ARG_CHECK(n_sigs > 0); + + if (!secp256k1_fullagg_session_load(ctx, fin_nonce, &fin_nonce_parity, &noncecoef, &n_signers, session)) { + return 0; + } + + ARG_CHECK(n_sigs == n_signers); + + /* Check if fin_nonce (R) is zero (infinity case) */ + is_fin_nonce_zero = secp256k1_is_zero_array(fin_nonce, 32); + + if (is_fin_nonce_zero) { + return 0; + } + + /* Aggregate all the s values */ + secp256k1_scalar_set_int(&s_agg, 0); + for (i = 0; i < n_sigs; i++) { + secp256k1_scalar s_i; + if (!secp256k1_fullagg_partial_sig_load(ctx, &s_i, partial_sigs[i])) { + return 0; + } + secp256k1_scalar_add(&s_agg, &s_agg, &s_i); + } + + /* Output aggregate signature */ + memcpy(sig64, fin_nonce, 32); + secp256k1_scalar_get_b32(&sig64[32], &s_agg); + + return 1; +} + +/* Verify a FullAgg aggregate signature */ +int secp256k1_fullagg_verify(const secp256k1_context* ctx, const unsigned char *sig64, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + size_t n_signers) { + secp256k1_scalar s; + secp256k1_ge r; + secp256k1_gej c_sum_pk; + secp256k1_gej tmp; + size_t i; + int overflow; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(sig64 != NULL); + ARG_CHECK(pubkeys != NULL); + ARG_CHECK(messages != NULL); + ARG_CHECK(n_signers > 0); + + /* Parse signature */ + if (!secp256k1_fe_set_b32_limit(&r.x, sig64)) { + return 0; + } + secp256k1_scalar_set_b32(&s, &sig64[32], &overflow); + if (overflow) { + return 0; + } + + /* Check even y */ + if (!secp256k1_ge_set_xo_var(&r, &r.x, 0)) { + return 0; + } + + /* Compute sum of c_i*pk_i */ + secp256k1_gej_set_infinity(&c_sum_pk); + for (i = 0; i < n_signers; i++) { + secp256k1_scalar c_i; + secp256k1_gej pkj; + secp256k1_ge pk_ge; + + if (!secp256k1_pubkey_load(ctx, &pk_ge, pubkeys[i])) { + return 0; + } + + /* Check that no public key is the identity element */ + if (secp256k1_ge_is_infinity(&pk_ge)) { + return 0; + } + + if (!secp256k1_fullagg_compute_sighash(ctx, &c_i, pubkeys, messages, n_signers, &r, i)) { + return 0; + } + secp256k1_gej_set_ge(&pkj, &pk_ge); + secp256k1_ecmult(&tmp, &pkj, &c_i, NULL); + secp256k1_gej_add_var(&c_sum_pk, &c_sum_pk, &tmp, NULL); + } + + /* Verify: s*G = R + sum(c_i*pk_i) */ + secp256k1_scalar_negate(&s, &s); + secp256k1_ecmult(&tmp, &c_sum_pk, &secp256k1_scalar_one, &s); + secp256k1_gej_add_ge_var(&tmp, &tmp, &r, NULL); + + return secp256k1_gej_is_infinity(&tmp); +} + +#endif /* SECP256K1_MODULE_SCHNORRSIG_FULLAGG_MAIN_H */ diff --git a/src/modules/schnorrsig_fullagg/tests_impl.h b/src/modules/schnorrsig_fullagg/tests_impl.h new file mode 100644 index 0000000000..a70912b62b --- /dev/null +++ b/src/modules/schnorrsig_fullagg/tests_impl.h @@ -0,0 +1,505 @@ +/*********************************************************************** + * Copyright (c) 2025 Fabian Jahr * + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_SCHNORRSIG_FULLAGG_TESTS_IMPL_H +#define SECP256K1_MODULE_SCHNORRSIG_FULLAGG_TESTS_IMPL_H + +#include +#include + +#include "../../../include/secp256k1.h" +#include "../../../include/secp256k1_extrakeys.h" +#include "../../../include/secp256k1_schnorrsig_fullagg.h" + +#include "../../scalar.h" +#include "../../field.h" +#include "../../group.h" +#include "../../hash.h" +#include "../../util.h" + +static int create_keypair_and_pk_fullagg(secp256k1_keypair *keypair, secp256k1_pubkey *pk, const unsigned char *sk) { + int ret; + secp256k1_keypair keypair_tmp; + ret = secp256k1_keypair_create(CTX, &keypair_tmp, sk); + ret &= secp256k1_keypair_pub(CTX, pk, &keypair_tmp); + if (keypair != NULL) { + *keypair = keypair_tmp; + } + return ret; +} + +/* Simple test with three signers */ +static void fullagg_simple_test(void) { + unsigned char sk[3][32]; + secp256k1_keypair keypair[3]; + secp256k1_fullagg_pubnonce pubnonce[3]; + const secp256k1_fullagg_pubnonce *pubnonce_ptr[3]; + secp256k1_fullagg_aggnonce aggnonce; + unsigned char msg[3][32]; + unsigned char session_secrand[3][32]; + secp256k1_fullagg_secnonce secnonce[3]; + secp256k1_pubkey pk[3]; + const secp256k1_pubkey *pk_ptr[3]; + const unsigned char *msg_ptr[3]; + secp256k1_fullagg_partial_sig partial_sig[3]; + const secp256k1_fullagg_partial_sig *partial_sig_ptr[3]; + unsigned char final_sig[64]; + secp256k1_fullagg_session session; + int i; + const secp256k1_pubkey *pk_wrong_order[3]; + int sign_success; + int retry_count = 0; + + testrand256(msg[0]); + testrand256(msg[1]); + testrand256(msg[2]); + + for (i = 0; i < 3; i++) { + testrand256(sk[i]); + pk_ptr[i] = &pk[i]; + msg_ptr[i] = msg[i]; + pubnonce_ptr[i] = &pubnonce[i]; + partial_sig_ptr[i] = &partial_sig[i]; + + CHECK(create_keypair_and_pk_fullagg(&keypair[i], &pk[i], sk[i])); + } + + /* Try up to 3 times in case we get unlucky with infinity aggregate nonces */ + sign_success = 0; + while (!sign_success && retry_count < 3) { + retry_count++; + + /* Generate nonces */ + for (i = 0; i < 3; i++) { + testrand256(session_secrand[i]); + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce[i], &pubnonce[i], session_secrand[i], + sk[i], &pk[i], msg[i], NULL) == 1); + } + + /* Aggregate nonces */ + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 3) == 1); + + /* Initialize session */ + CHECK(secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 3) == 1); + + /* The signers create their partial signature */ + sign_success = 1; + for (i = 0; i < 3; i++) { + if (secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce[i], + &keypair[i], &session, pk_ptr, msg_ptr, + pubnonce_ptr, i) != 1) { + sign_success = 0; + break; + } + /* Verify partial signature */ + if (secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[i], &pubnonce[i], + &pk[i], &session, pk_ptr, msg_ptr, i) != 1) { + sign_success = 0; + break; + } + } + } + CHECK(sign_success); + + /* Aggregate partial signatures */ + CHECK(secp256k1_fullagg_partial_sig_agg(CTX, final_sig, &session, partial_sig_ptr, 3) == 1); + + /* Verify the aggregate signature */ + CHECK(secp256k1_fullagg_verify(CTX, final_sig, pk_ptr, msg_ptr, 3) == 1); + + /* Test that verification fails with wrong message */ + msg[0][0] ^= 1; /* Maleate the message */ + CHECK(secp256k1_fullagg_verify(CTX, final_sig, pk_ptr, msg_ptr, 3) == 0); + msg[0][0] ^= 1; /* Restore original message */ + + /* Test that verification fails with wrong public key order */ + pk_wrong_order[0] = &pk[1]; + pk_wrong_order[1] = &pk[0]; + pk_wrong_order[2] = &pk[2]; + CHECK(secp256k1_fullagg_verify(CTX, final_sig, pk_wrong_order, msg_ptr, 3) == 0); + + /* Test that verification fails with incomplete list */ + CHECK(secp256k1_fullagg_verify(CTX, final_sig, pk_ptr, msg_ptr, 2) == 0); +} + +/* Test API parameter validation */ +static void fullagg_api_tests(void) { + secp256k1_fullagg_partial_sig partial_sig[2]; + const secp256k1_fullagg_partial_sig *partial_sig_ptr[2]; + unsigned char pre_sig[64]; + unsigned char sk[2][32]; + secp256k1_keypair keypair[2]; + unsigned char zeros132[132] = { 0 }; + unsigned char session_secrand[2][32]; + secp256k1_fullagg_secnonce secnonce[2]; + secp256k1_fullagg_pubnonce pubnonce[2]; + const secp256k1_fullagg_pubnonce *pubnonce_ptr[2]; + unsigned char pubnonce_ser[66]; + secp256k1_fullagg_aggnonce aggnonce; + unsigned char aggnonce_ser[66]; + unsigned char msg[2][32]; + const unsigned char *msg_ptr[2]; + secp256k1_fullagg_session session; + secp256k1_pubkey pk[2]; + const secp256k1_pubkey *pk_ptr[2]; + int i; + + for (i = 0; i < 2; i++) { + pk_ptr[i] = &pk[i]; + msg_ptr[i] = msg[i]; + pubnonce_ptr[i] = &pubnonce[i]; + partial_sig_ptr[i] = &partial_sig[i]; + testrand256(session_secrand[i]); + testrand256(sk[i]); + testrand256(msg[i]); + CHECK(create_keypair_and_pk_fullagg(&keypair[i], &pk[i], sk[i])); + } + + /* Nonce generation */ + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce[0], &pubnonce[0], session_secrand[0], + sk[0], &pk[0], msg[0], NULL) == 1); + /* Check that session_secrand is zeroed */ + CHECK(secp256k1_memcmp_var(session_secrand[0], zeros132, sizeof(session_secrand[0])) == 0); + + /* session_secrand = 0 is disallowed */ + memset(session_secrand[0], 0, sizeof(session_secrand[0])); + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce[0], &pubnonce[0], session_secrand[0], + sk[0], &pk[0], msg[0], NULL) == 0); + + /* Test NULL parameters */ + testrand256(session_secrand[0]); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_gen(CTX, NULL, &pubnonce[0], session_secrand[0], + sk[0], &pk[0], msg[0], NULL)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_gen(CTX, &secnonce[0], NULL, session_secrand[0], + sk[0], &pk[0], msg[0], NULL)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_gen(CTX, &secnonce[0], &pubnonce[0], NULL, + sk[0], &pk[0], msg[0], NULL)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_gen(CTX, &secnonce[0], &pubnonce[0], session_secrand[0], + sk[0], NULL, msg[0], NULL)); + + /* Generate valid nonces for both signers */ + for (i = 0; i < 2; i++) { + testrand256(session_secrand[i]); + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce[i], &pubnonce[i], session_secrand[i], + sk[i], &pk[i], msg[i], NULL) == 1); + } + + /* Serialize and parse public nonces */ + CHECK(secp256k1_fullagg_pubnonce_serialize(CTX, pubnonce_ser, &pubnonce[0]) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_pubnonce_serialize(CTX, NULL, &pubnonce[0])); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_pubnonce_serialize(CTX, pubnonce_ser, NULL)); + + CHECK(secp256k1_fullagg_pubnonce_parse(CTX, &pubnonce[0], pubnonce_ser) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_pubnonce_parse(CTX, NULL, pubnonce_ser)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_pubnonce_parse(CTX, &pubnonce[0], NULL)); + + /* Nonce aggregation */ + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 2) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_agg(CTX, NULL, pubnonce_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_agg(CTX, &aggnonce, NULL, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 0)); + + /* Serialize and parse aggregate nonces */ + CHECK(secp256k1_fullagg_aggnonce_serialize(CTX, aggnonce_ser, &aggnonce) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_aggnonce_serialize(CTX, NULL, &aggnonce)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_aggnonce_serialize(CTX, aggnonce_ser, NULL)); + + CHECK(secp256k1_fullagg_aggnonce_parse(CTX, &aggnonce, aggnonce_ser) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_aggnonce_parse(CTX, NULL, aggnonce_ser)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_aggnonce_parse(CTX, &aggnonce, NULL)); + + /* Session initialization */ + CHECK(secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 2) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_session_init(CTX, NULL, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_session_init(CTX, &session, NULL, pk_ptr, msg_ptr, + pubnonce_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_session_init(CTX, &session, &aggnonce, NULL, msg_ptr, + pubnonce_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, NULL, + pubnonce_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + NULL, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 0)); + + /* Partial signing */ + { + int sign_success = 1; + for (i = 0; i < 2; i++) { + secp256k1_fullagg_secnonce secnonce_tmp; + memcpy(&secnonce_tmp, &secnonce[i], sizeof(secnonce_tmp)); + if (secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce_tmp, &keypair[i], + &session, pk_ptr, msg_ptr, pubnonce_ptr, i) != 1) { + sign_success = 0; + break; + } + /* The secnonce is set to 0 and following signing attempts fail */ + CHECK(secp256k1_memcmp_var(&secnonce_tmp, zeros132, sizeof(secnonce_tmp)) == 0); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce_tmp, + &keypair[i], &session, pk_ptr, msg_ptr, + pubnonce_ptr, i)); + } + + /* If signing failed due to infinity aggregate nonce, regenerate and try once more */ + if (!sign_success) { + /* Generate new nonces */ + for (i = 0; i < 2; i++) { + testrand256(session_secrand[i]); + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce[i], &pubnonce[i], session_secrand[i], + sk[i], &pk[i], msg[i], NULL) == 1); + } + + /* Re-aggregate and re-initialize session */ + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 2) == 1); + CHECK(secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 2) == 1); + + /* Try signing again */ + for (i = 0; i < 2; i++) { + secp256k1_fullagg_secnonce secnonce_tmp; + memcpy(&secnonce_tmp, &secnonce[i], sizeof(secnonce_tmp)); + CHECK(secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce_tmp, &keypair[i], + &session, pk_ptr, msg_ptr, pubnonce_ptr, i) == 1); + /* The secnonce is set to 0 and subsequent signing attempts fail */ + CHECK(secp256k1_memcmp_var(&secnonce_tmp, zeros132, sizeof(secnonce_tmp)) == 0); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce_tmp, + &keypair[i], &session, pk_ptr, msg_ptr, + pubnonce_ptr, i)); + } + } + } + + /* Regenerate nonces and sign again with fresh secnonces */ + { + int sign_success = 0; + int retry_count = 0; + /* Try up to 3 times in case we get unlucky with infinity aggregate nonces */ + while (!sign_success && retry_count < 3) { + retry_count++; + + /* Generate new nonces */ + for (i = 0; i < 2; i++) { + testrand256(session_secrand[i]); + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce[i], &pubnonce[i], session_secrand[i], + sk[i], &pk[i], msg[i], NULL) == 1); + } + + /* Re-aggregate nonces and re-initialize session with new nonces */ + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 2) == 1); + CHECK(secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 2) == 1); + + /* Try signing with the fresh nonces */ + sign_success = 1; + for (i = 0; i < 2; i++) { + if (secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce[i], &keypair[i], + &session, pk_ptr, msg_ptr, pubnonce_ptr, i) != 1) { + sign_success = 0; + break; + } + } + } + CHECK(sign_success); /* Should succeed within 3 attempts */ + } + + /** Partial signature verification **/ + CHECK(secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[0], &pubnonce[0], &pk[0], + &session, pk_ptr, msg_ptr, 0) == 1); + CHECK(secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[1], &pubnonce[1], &pk[1], + &session, pk_ptr, msg_ptr, 1) == 1); + /* Wrong signer index */ + CHECK(secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[0], &pubnonce[0], &pk[0], + &session, pk_ptr, msg_ptr, 1) == 0); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_verify(CTX, NULL, &pubnonce[0], &pk[0], + &session, pk_ptr, msg_ptr, 0)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[0], NULL, &pk[0], + &session, pk_ptr, msg_ptr, 0)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[0], &pubnonce[0], NULL, + &session, pk_ptr, msg_ptr, 0)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_verify(CTX, &partial_sig[0], &pubnonce[0], &pk[0], + NULL, pk_ptr, msg_ptr, 0)); + + /** Signature aggregation **/ + CHECK(secp256k1_fullagg_partial_sig_agg(CTX, pre_sig, &session, partial_sig_ptr, 2) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_agg(CTX, NULL, &session, partial_sig_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_agg(CTX, pre_sig, NULL, partial_sig_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_agg(CTX, pre_sig, &session, NULL, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_partial_sig_agg(CTX, pre_sig, &session, partial_sig_ptr, 0)); + + /** Verification **/ + CHECK(secp256k1_fullagg_verify(CTX, pre_sig, pk_ptr, msg_ptr, 2) == 1); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_verify(CTX, NULL, pk_ptr, msg_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_verify(CTX, pre_sig, NULL, msg_ptr, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_verify(CTX, pre_sig, pk_ptr, NULL, 2)); + CHECK_ILLEGAL(CTX, secp256k1_fullagg_verify(CTX, pre_sig, pk_ptr, msg_ptr, 0)); +} + +/* Test with counter-based nonce generation */ +static void fullagg_nonce_gen_counter_test(void) { + unsigned char sk[2][32]; + secp256k1_keypair keypair[2]; + secp256k1_pubkey pk[2]; + const secp256k1_pubkey *pk_ptr[2]; + secp256k1_fullagg_pubnonce pubnonce[2]; + const secp256k1_fullagg_pubnonce *pubnonce_ptr[2]; + secp256k1_fullagg_aggnonce aggnonce; + secp256k1_fullagg_secnonce secnonce[2]; + secp256k1_fullagg_session session; + secp256k1_fullagg_partial_sig partial_sig[2]; + const secp256k1_fullagg_partial_sig *partial_sig_ptr[2]; + unsigned char msg[2][32]; + const unsigned char *msg_ptr[2]; + unsigned char final_sig[64]; + uint64_t nonrepeating_cnt = 0; + int i; + int sign_success; + int retry_count; + + for (i = 0; i < 2; i++) { + testrand256(sk[i]); + testrand256(msg[i]); + CHECK(create_keypair_and_pk_fullagg(&keypair[i], &pk[i], sk[i])); + pk_ptr[i] = &pk[i]; + msg_ptr[i] = msg[i]; + pubnonce_ptr[i] = &pubnonce[i]; + partial_sig_ptr[i] = &partial_sig[i]; + } + + sign_success = 0; + retry_count = 0; + while (!sign_success && retry_count < 3) { + retry_count++; + + /* Generate nonces using counter */ + for (i = 0; i < 2; i++) { + CHECK(secp256k1_fullagg_nonce_gen_counter(CTX, &secnonce[i], &pubnonce[i], + nonrepeating_cnt + i, &keypair[i], + msg[i], NULL) == 1); + } + nonrepeating_cnt += 2; /* Increment for next attempt if needed */ + + /* Complete the protocol */ + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 2) == 1); + CHECK(secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 2) == 1); + + sign_success = 1; + for (i = 0; i < 2; i++) { + if (secp256k1_fullagg_partial_sign(CTX, &partial_sig[i], &secnonce[i], &keypair[i], + &session, pk_ptr, msg_ptr, pubnonce_ptr, i) != 1) { + sign_success = 0; + break; + } + } + } + CHECK(sign_success); + + CHECK(secp256k1_fullagg_partial_sig_agg(CTX, final_sig, &session, partial_sig_ptr, 2) == 1); + CHECK(secp256k1_fullagg_verify(CTX, final_sig, pk_ptr, msg_ptr, 2) == 1); +} + +/* Test serialization round-trip */ +static void fullagg_serialization_test(void) { + secp256k1_fullagg_pubnonce pubnonce, pubnonce2; + secp256k1_fullagg_aggnonce aggnonce, aggnonce2; + secp256k1_fullagg_partial_sig partial_sig, partial_sig2; + unsigned char pubnonce_ser[66]; + unsigned char aggnonce_ser[66]; + unsigned char partial_sig_ser[32]; + unsigned char session_secrand[32]; + unsigned char sk[32]; + unsigned char msg[32]; + secp256k1_pubkey pk; + secp256k1_fullagg_secnonce secnonce; + secp256k1_scalar s; + const secp256k1_fullagg_pubnonce *pubnonce_ptr; + + testrand256(sk); + testrand256(msg); + testrand256(session_secrand); + CHECK(create_keypair_and_pk_fullagg(NULL, &pk, sk)); + + /* Test pubnonce serialization */ + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce, &pubnonce, session_secrand, + sk, &pk, msg, NULL) == 1); + CHECK(secp256k1_fullagg_pubnonce_serialize(CTX, pubnonce_ser, &pubnonce) == 1); + CHECK(secp256k1_fullagg_pubnonce_parse(CTX, &pubnonce2, pubnonce_ser) == 1); + CHECK(secp256k1_memcmp_var(&pubnonce, &pubnonce2, sizeof(pubnonce)) == 0); + + /* Test aggnonce serialization */ + pubnonce_ptr = &pubnonce; + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, &pubnonce_ptr, 1) == 1); + CHECK(secp256k1_fullagg_aggnonce_serialize(CTX, aggnonce_ser, &aggnonce) == 1); + CHECK(secp256k1_fullagg_aggnonce_parse(CTX, &aggnonce2, aggnonce_ser) == 1); + CHECK(secp256k1_memcmp_var(&aggnonce, &aggnonce2, sizeof(aggnonce)) == 0); + + /* Test partial_sig serialization */ + testutil_random_scalar_order_test(&s); + secp256k1_fullagg_partial_sig_save(&partial_sig, &s); + CHECK(secp256k1_fullagg_partial_sig_serialize(CTX, partial_sig_ser, &partial_sig) == 1); + CHECK(secp256k1_fullagg_partial_sig_parse(CTX, &partial_sig2, partial_sig_ser) == 1); + CHECK(secp256k1_memcmp_var(&partial_sig, &partial_sig2, sizeof(partial_sig)) == 0); +} + +static void fullagg_duplicate_pubnonce_test(void) { + unsigned char sk[32]; + secp256k1_keypair keypair; + secp256k1_pubkey pk; + const secp256k1_pubkey *pk_ptr[2]; + secp256k1_fullagg_pubnonce pubnonce[2]; + const secp256k1_fullagg_pubnonce *pubnonce_ptr[2]; + secp256k1_fullagg_aggnonce aggnonce; + secp256k1_fullagg_secnonce secnonce; + secp256k1_fullagg_session session; + secp256k1_fullagg_partial_sig partial_sig; + unsigned char msg[2][32]; + const unsigned char *msg_ptr[2]; + unsigned char session_secrand[32]; + + testrand256(sk); + testrand256(msg[0]); + testrand256(msg[1]); + CHECK(create_keypair_and_pk_fullagg(&keypair, &pk, sk)); + + testrand256(session_secrand); + CHECK(secp256k1_fullagg_nonce_gen(CTX, &secnonce, &pubnonce[0], session_secrand, + sk, &pk, msg[0], NULL) == 1); + + /* Duplicate the pubnonce */ + pubnonce[1] = pubnonce[0]; + + pk_ptr[0] = &pk; + pk_ptr[1] = &pk; + msg_ptr[0] = msg[0]; + msg_ptr[1] = msg[1]; + pubnonce_ptr[0] = &pubnonce[0]; + pubnonce_ptr[1] = &pubnonce[1]; + + CHECK(secp256k1_fullagg_nonce_agg(CTX, &aggnonce, pubnonce_ptr, 2) == 1); + CHECK(secp256k1_fullagg_session_init(CTX, &session, &aggnonce, pk_ptr, msg_ptr, + pubnonce_ptr, 2) == 1); + + /* Fails because R2 appears twice */ + CHECK(secp256k1_fullagg_partial_sign(CTX, &partial_sig, &secnonce, &keypair, + &session, pk_ptr, msg_ptr, pubnonce_ptr, 0) == 0); +} + +static void run_schnorrsig_fullagg_tests(void) { + int i; + + for (i = 0; i < COUNT; i++) { + fullagg_simple_test(); + } + fullagg_api_tests(); + fullagg_nonce_gen_counter_test(); + fullagg_serialization_test(); + fullagg_duplicate_pubnonce_test(); +} + +#endif /* SECP256K1_MODULE_SCHNORRSIG_FULLAGG_TESTS_IMPL_H */ diff --git a/src/secp256k1.c b/src/secp256k1.c index 26336a45cc..a5eefa5c84 100644 --- a/src/secp256k1.c +++ b/src/secp256k1.c @@ -810,6 +810,10 @@ int secp256k1_tagged_sha256(const secp256k1_context* ctx, unsigned char *hash32, # include "modules/schnorrsig/main_impl.h" #endif +#ifdef ENABLE_MODULE_SCHNORRSIG_FULLAGG +# include "modules/schnorrsig_fullagg/main_impl.h" +#endif + #ifdef ENABLE_MODULE_MUSIG # include "modules/musig/main_impl.h" #endif diff --git a/src/tests.c b/src/tests.c index cb3b3c4248..a9a63dbed5 100644 --- a/src/tests.c +++ b/src/tests.c @@ -7434,6 +7434,10 @@ static void run_ecdsa_wycheproof(void) { # include "modules/schnorrsig/tests_impl.h" #endif +#ifdef ENABLE_MODULE_SCHNORRSIG_FULLAGG +# include "modules/schnorrsig_fullagg/tests_impl.h" +#endif + #ifdef ENABLE_MODULE_MUSIG # include "modules/musig/tests_impl.h" #endif @@ -7802,6 +7806,10 @@ int main(int argc, char **argv) { run_schnorrsig_tests(); #endif +#ifdef ENABLE_MODULE_SCHNORRSIG_FULLAGG + run_schnorrsig_fullagg_tests(); +#endif + #ifdef ENABLE_MODULE_MUSIG run_musig_tests(); #endif From 2d8c1b10abdade79d023058726b1248c795b75c5 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 12 Sep 2025 17:55:33 +0200 Subject: [PATCH 3/7] fullagg: Add include file --- include/secp256k1_schnorrsig_fullagg.h | 433 +++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 include/secp256k1_schnorrsig_fullagg.h diff --git a/include/secp256k1_schnorrsig_fullagg.h b/include/secp256k1_schnorrsig_fullagg.h new file mode 100644 index 0000000000..80a5036a71 --- /dev/null +++ b/include/secp256k1_schnorrsig_fullagg.h @@ -0,0 +1,433 @@ +#ifndef SECP256K1_FULLAGG_H +#define SECP256K1_FULLAGG_H + +#include "secp256k1_extrakeys.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** This module implements Full Aggregation (FullAgg/DahLIAS) for Schnorr + * signatures, allowing multiple signers to each sign different messages + * and aggregate their signatures into a single signature. + */ + +/** Opaque data structures + * + * The exact representation of data inside the opaque data structures is + * implementation defined and not guaranteed to be portable between different + * platforms or versions. With the exception of `secp256k1_fullagg_secnonce`, + * the data structures can be safely copied/moved. If you need to convert to a + * format suitable for storage, transmission, or comparison, use the + * corresponding serialization and parsing functions. + */ + +/** Opaque data structure that holds a signer's _secret_ nonce (r1_i and r2_i). + * + * Guaranteed to be 132 bytes in size. + * + * WARNING: This structure MUST NOT be copied or read or written to directly. + * A signer who is online throughout the whole process and can keep this + * structure in memory can use the provided API functions for a safe standard + * workflow. + * + * Copying this data structure can result in nonce reuse which will leak the + * secret signing key. + */ +typedef struct secp256k1_fullagg_secnonce { + unsigned char data[132]; +} secp256k1_fullagg_secnonce; + +/** Opaque data structure that holds a signer's public nonce (R1_i and R2_i). + * + * Guaranteed to be 132 bytes in size. Serialized and parsed with + * `fullagg_pubnonce_serialize` and `fullagg_pubnonce_parse`. + */ +typedef struct secp256k1_fullagg_pubnonce { + unsigned char data[132]; +} secp256k1_fullagg_pubnonce; + +/** Opaque data structure that holds an aggregate public nonce (aggregated R1 and R2). + * + * Guaranteed to be 132 bytes in size. Serialized and parsed with + * `fullagg_aggnonce_serialize` and `fullagg_aggnonce_parse`. + */ +typedef struct secp256k1_fullagg_aggnonce { + unsigned char data[132]; +} secp256k1_fullagg_aggnonce; + +/** Opaque data structure that holds a FullAgg session. + * + * This structure contains the computed values needed for signing: + * the final nonce R, the nonce coefficient b, and the number of signers. + * + * Guaranteed to be 77 bytes in size. + */ +typedef struct secp256k1_fullagg_session { + unsigned char data[77]; +} secp256k1_fullagg_session; + +/** Opaque data structure that holds a partial FullAgg signature (s_i). + * + * Guaranteed to be 36 bytes in size. Serialized and parsed with + * `fullagg_partial_sig_serialize` and `fullagg_partial_sig_parse`. + */ +typedef struct secp256k1_fullagg_partial_sig { + unsigned char data[36]; +} secp256k1_fullagg_partial_sig; + +/** Parse a signer's public nonce. + * + * Returns: 1 when the nonce could be parsed, 0 otherwise. + * Args: ctx: pointer to a context object + * Out: nonce: pointer to a nonce object + * In: in66: pointer to the 66-byte nonce to be parsed + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_pubnonce_parse( + const secp256k1_context *ctx, + secp256k1_fullagg_pubnonce *nonce, + const unsigned char *in66 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a signer's public nonce + * + * Returns: 1 always + * Args: ctx: pointer to a context object + * Out: out66: pointer to a 66-byte array to store the serialized nonce + * In: nonce: pointer to the nonce + */ +SECP256K1_API int secp256k1_fullagg_pubnonce_serialize( + const secp256k1_context *ctx, + unsigned char *out66, + const secp256k1_fullagg_pubnonce *nonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse an aggregate public nonce. + * + * Returns: 1 when the nonce could be parsed, 0 otherwise. + * Args: ctx: pointer to a context object + * Out: nonce: pointer to a nonce object + * In: in66: pointer to the 66-byte nonce to be parsed + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_aggnonce_parse( + const secp256k1_context *ctx, + secp256k1_fullagg_aggnonce *nonce, + const unsigned char *in66 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize an aggregate public nonce + * + * Returns: 1 always + * Args: ctx: pointer to a context object + * Out: out66: pointer to a 66-byte array to store the serialized nonce + * In: nonce: pointer to the nonce + */ +SECP256K1_API int secp256k1_fullagg_aggnonce_serialize( + const secp256k1_context *ctx, + unsigned char *out66, + const secp256k1_fullagg_aggnonce *nonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse a FullAgg partial signature. + * + * Returns: 1 when the signature could be parsed, 0 otherwise. + * Args: ctx: pointer to a context object + * Out: sig: pointer to a signature object + * In: in32: pointer to the 32-byte signature to be parsed + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_partial_sig_parse( + const secp256k1_context *ctx, + secp256k1_fullagg_partial_sig *sig, + const unsigned char *in32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a FullAgg partial signature + * + * Returns: 1 always + * Args: ctx: pointer to a context object + * Out: out32: pointer to a 32-byte array to store the serialized signature + * In: sig: pointer to the signature + */ +SECP256K1_API int secp256k1_fullagg_partial_sig_serialize( + const secp256k1_context *ctx, + unsigned char *out32, + const secp256k1_fullagg_partial_sig *sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Starts a signing session by generating a nonce + * + * This function outputs a secret nonce that will be required for signing and a + * corresponding public nonce that is intended to be sent to the coordinator. + * + * FullAgg differs from regular Schnorr signing in that implementers _must_ take + * special care to not reuse a nonce. This can be ensured by following these rules: + * + * 1. Each call to this function must have a UNIQUE session_secrand32 that must + * NOT BE REUSED in subsequent calls to this function and must be KEPT + * SECRET (even from other signers). + * 2. If you already know the seckey, message or public key, they can be + * optionally provided to derive the nonce and increase misuse-resistance. + * The extra_input32 argument can be used to provide additional data that + * does not repeat in normal scenarios, such as the current time. + * 3. Avoid copying (or serializing) the secnonce. This reduces the possibility + * that it is used more than once for signing. + * + * If you don't have access to good randomness for session_secrand32, but you + * have access to a non-repeating counter, then see + * secp256k1_fullagg_nonce_gen_counter. + * + * Remember that nonce reuse will leak the secret key! + * Note that using the same seckey for multiple FullAgg sessions is fine. + * + * Returns: 0 if the arguments are invalid and 1 otherwise + * Args: ctx: pointer to a context object (not secp256k1_context_static) + * Out: secnonce: pointer to a structure to store the secret nonce (r1_i, r2_i) + * pubnonce: pointer to a structure to store the public nonce (R1_i, R2_i) + * In/Out: + * session_secrand32: a 32-byte session_secrand32 as explained above. Must be unique to + * this call to secp256k1_fullagg_nonce_gen and must be + * uniformly random. If the function call is successful, the + * session_secrand32 buffer is invalidated to prevent reuse. + * In: + * seckey: the 32-byte secret key that will later be used for signing, if + * already known (can be NULL) + * pubkey: public key of the signer creating the nonce. The secnonce + * output of this function cannot be used to sign for any + * other public key. While the public key should correspond + * to the provided seckey, a mismatch will not cause the + * function to return 0. + * msg32: the 32-byte message that this signer will later sign, if + * already known (can be NULL) + * extra_input32: an optional 32-byte array that is input to the nonce + * derivation function (can be NULL) + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_nonce_gen( + const secp256k1_context *ctx, + secp256k1_fullagg_secnonce *secnonce, + secp256k1_fullagg_pubnonce *pubnonce, + unsigned char *session_secrand32, + const unsigned char *seckey, + const secp256k1_pubkey *pubkey, + const unsigned char *msg32, + const unsigned char *extra_input32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6); + +/** Alternative way to generate a nonce and start a signing session + * + * This function outputs a secret nonce that will be required for signing and a + * corresponding public nonce that is intended to be sent to the coordinator. + * + * This function differs from `secp256k1_fullagg_nonce_gen` by accepting a + * non-repeating counter value instead of a secret random value. This requires + * that a secret key is provided to `secp256k1_fullagg_nonce_gen_counter` + * (through the keypair argument). + * + * Returns: 0 if the arguments are invalid and 1 otherwise + * Args: ctx: pointer to a context object (not secp256k1_context_static) + * Out: secnonce: pointer to a structure to store the secret nonce + * pubnonce: pointer to a structure to store the public nonce + * In: + * nonrepeating_cnt: the value of a counter as explained above. Must be + * unique to this call to secp256k1_fullagg_nonce_gen. + * keypair: keypair of the signer creating the nonce. The secnonce + * output of this function cannot be used to sign for any + * other keypair. + * msg32: the 32-byte message that this signer will later sign, if + * already known (can be NULL) + * extra_input32: an optional 32-byte array that is input to the nonce + * derivation function (can be NULL) + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_nonce_gen_counter( + const secp256k1_context *ctx, + secp256k1_fullagg_secnonce *secnonce, + secp256k1_fullagg_pubnonce *pubnonce, + uint64_t nonrepeating_cnt, + const secp256k1_keypair *keypair, + const unsigned char *msg32, + const unsigned char *extra_input32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5); + +/** Aggregates the nonces of all signers into a single nonce + * + * This is done by the coordinator to compute the aggregate nonces R1 and R2 + * from all signers' individual nonces R1_i and R2_i. + * + * If the aggregator does not compute the aggregate nonce correctly, the final + * signature will be invalid. + * + * Returns: 0 if the arguments are invalid, 1 otherwise + * Args: ctx: pointer to a context object + * Out: aggnonce: pointer to an aggregate public nonce object for + * fullagg_session_init (contains R1 and R2) + * In: pubnonces: array of pointers to public nonces sent by the + * signers (each containing R1_i and R2_i) + * n_pubnonces: number of elements in the pubnonces array. Must be + * greater than 0. + */ +SECP256K1_API int secp256k1_fullagg_nonce_agg( + const secp256k1_context *ctx, + secp256k1_fullagg_aggnonce *aggnonce, + const secp256k1_fullagg_pubnonce * const *pubnonces, + size_t n_pubnonces +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Initialize a FullAgg signing session + * + * Creates a session context that contains the computed values needed for + * signing and verification of partial signatures. This includes the final + * nonce R and the nonce coefficient b. The arrays of public keys, messages, + * and nonces must be provided again when signing or verifying. + * + * Returns: 0 if the arguments are invalid, 1 otherwise + * Args: ctx: pointer to a context object + * Out: session: pointer to a struct to store the session + * In: aggnonce: pointer to an aggregate public nonce object that is the + * output of fullagg_nonce_agg + * pubkeys: array of pointers to the public keys of all signers. + * The order must be consistent across all signers and + * the coordinator. + * messages: array of pointers to 32-byte messages, where messages[i] + * is the message to be signed by the signer with pubkeys[i] + * pubnonces: array of pointers to public nonces, where pubnonces[i] + * contains the R2_i value from the signer with pubkeys[i]. + * This is needed for the context. + * n_signers: number of signers (length of pubkeys, messages, and + * pubnonces arrays). Must be greater than 0. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_session_init( + const secp256k1_context *ctx, + secp256k1_fullagg_session *session, + const secp256k1_fullagg_aggnonce *aggnonce, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + const secp256k1_fullagg_pubnonce * const *pubnonces, + size_t n_signers +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6); + +/** Produces a partial signature + * + * This function overwrites the given secnonce with zeros and will abort if given a + * secnonce that is all zeros. This is a best effort attempt to protect against nonce + * reuse. However, this is of course easily defeated if the secnonce has been + * copied (or serialized). Remember that nonce reuse will leak the secret key! + * + * For signing to succeed, the secnonce provided to this function must have + * been generated for the provided keypair. + * + * The signer must ensure that their public key, message, and R2_i nonce + * appear exactly once in the arrays at the expected position. + * + * Returns: 0 if the arguments are invalid or the provided secnonce has already + * been used for signing, 1 otherwise + * Args: ctx: pointer to a context object + * Out: partial_sig: pointer to struct to store the partial signature + * In/Out: secnonce: pointer to the secnonce struct created in + * fullagg_nonce_gen that has never been used in a + * partial_sign call before + * In: keypair: pointer to keypair to sign the message with + * session: pointer to the session that was created with + * fullagg_session_init + * pubkeys: array of pointers to public keys (same as in session_init) + * messages: array of pointers to messages (same as in session_init) + * pubnonces: array of pointers to public nonces (same as in session_init) + * signer_index: the index of this signer in the arrays (0-indexed) + */ +SECP256K1_API int secp256k1_fullagg_partial_sign( + const secp256k1_context *ctx, + secp256k1_fullagg_partial_sig *partial_sig, + secp256k1_fullagg_secnonce *secnonce, + const secp256k1_keypair *keypair, + const secp256k1_fullagg_session *session, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + const secp256k1_fullagg_pubnonce * const *pubnonces, + size_t signer_index +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8); + +/** Verifies an individual signer's partial signature + * + * The signature is verified for a specific signing session. + * + * It is not required to call this function in regular FullAgg sessions, + * because if any partial signature does not verify, the final signature + * will not verify either. However, this function provides the ability to + * identify which specific partial signature fails verification. + * + * Note: The arrays of public keys and messages must be provided again + * (in the same order as in session_init) because the session only stores the + * computed values, not the full context. + * + * Returns: 0 if the arguments are invalid or the partial signature does not + * verify, 1 otherwise + * Args ctx: pointer to a context object + * In: partial_sig: pointer to partial signature to verify + * pubnonce: public nonce of the signer being verified + * pubkey: public key of the signer being verified + * session: pointer to the session that was created with + * fullagg_session_init + * pubkeys: array of pointers to public keys (same as in session_init) + * messages: array of pointers to messages (same as in session_init) + * signer_index: the index of the signer being verified in the arrays + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_partial_sig_verify( + const secp256k1_context *ctx, + const secp256k1_fullagg_partial_sig *partial_sig, + const secp256k1_fullagg_pubnonce *pubnonce, + const secp256k1_pubkey *pubkey, + const secp256k1_fullagg_session *session, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + size_t signer_index +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7); + +/** Aggregates partial signatures + * + * Produces the final aggregated signature from all partial signatures. + * + * Returns: 0 if the arguments are invalid, 1 otherwise (which does NOT mean + * the resulting signature verifies). + * Args: ctx: pointer to a context object + * Out: sig64: complete (but possibly invalid) Schnorr signature + * In: session: pointer to the session that was created with + * fullagg_session_init + * partial_sigs: array of pointers to partial signatures to aggregate + * n_sigs: number of elements in the partial_sigs array. Must be + * greater than 0 and equal to the number of signers. + */ +SECP256K1_API int secp256k1_fullagg_partial_sig_agg( + const secp256k1_context *ctx, + unsigned char *sig64, + const secp256k1_fullagg_session *session, + const secp256k1_fullagg_partial_sig * const *partial_sigs, + size_t n_sigs +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Verify a FullAgg aggregate signature + * + * Verifies that the signature is valid for the given list of public keys + * and their corresponding messages. + * + * Returns: 1 if the signature is valid, 0 otherwise + * Args: ctx: pointer to a context object + * In: sig64: pointer to the 64-byte signature to verify + * pubkeys: array of pointers to public keys + * messages: array of pointers to 32-byte messages, where messages[i] + * corresponds to pubkeys[i] + * n_signers: number of signers (length of pubkeys and messages arrays) + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_fullagg_verify( + const secp256k1_context *ctx, + const unsigned char *sig64, + const secp256k1_pubkey * const *pubkeys, + const unsigned char * const *messages, + size_t n_signers +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +#ifdef __cplusplus +} +#endif + +#endif From 5968aeb6426792cac5c044a9ed3d87962deacac8 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 12 Sep 2025 17:55:53 +0200 Subject: [PATCH 4/7] fullagg: Add example --- .gitignore | 1 + examples/CMakeLists.txt | 4 + examples/fullagg.c | 309 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 314 insertions(+) create mode 100644 examples/fullagg.c diff --git a/.gitignore b/.gitignore index ce33a84adf..645f391a22 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ ecdsa_example schnorr_example ellswift_example musig_example +fullagg_example *.exe *.so *.a diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c9da9de6be..665ca483f2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -29,3 +29,7 @@ endif() if(SECP256K1_ENABLE_MODULE_MUSIG) add_example(musig) endif() + +if(SECP256K1_ENABLE_MODULE_SCHNORRSIG_FULLAGG) + add_example(fullagg) +endif() diff --git a/examples/fullagg.c b/examples/fullagg.c new file mode 100644 index 0000000000..c752d70f80 --- /dev/null +++ b/examples/fullagg.c @@ -0,0 +1,309 @@ +/************************************************************************* + * Written in 2025 by Fabian Jahr * + * To the extent possible under law, the author(s) have dedicated all * + * copyright and related and neighboring rights to the software in this * + * file to the public domain worldwide. This software is distributed * + * without any warranty. For the CC0 Public Domain Dedication, see * + * EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 * + *************************************************************************/ + +/** This file demonstrates how to use the FullAgg module to create an + * aggregate signature where each signer signs a different message. + * + * FullAgg (DahLIAS) allows multiple signers to create a single aggregate + * signature that proves each signer signed their respective message. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "examples_util.h" + +#define N_SIGNERS 3 + +struct signer_secrets { + secp256k1_keypair keypair; + secp256k1_fullagg_secnonce secnonce; +}; + +struct signer { + secp256k1_pubkey pubkey; + secp256k1_fullagg_pubnonce pubnonce; + secp256k1_fullagg_partial_sig partial_sig; + unsigned char message[32]; +}; + +/* Create a key pair, store it in signer_secrets->keypair and signer->pubkey */ +static int create_keypair(const secp256k1_context* ctx, struct signer_secrets *signer_secrets, struct signer *signer) { + unsigned char seckey[32]; + + if (!fill_random(seckey, sizeof(seckey))) { + printf("Failed to generate randomness\n"); + return 0; + } + /* Try to create a keypair with a valid context. This only fails if the + * secret key is zero or out of range (greater than secp256k1's order). Note + * that the probability of this occurring is negligible with a properly + * functioning random number generator. */ + if (!secp256k1_keypair_create(ctx, &signer_secrets->keypair, seckey)) { + return 0; + } + if (!secp256k1_keypair_pub(ctx, &signer->pubkey, &signer_secrets->keypair)) { + return 0; + } + + secure_erase(seckey, sizeof(seckey)); + return 1; +} + +static void setup_messages(struct signer *signers) { + memset(signers[0].message, 0, 32); + memset(signers[1].message, 0, 32); + memset(signers[2].message, 0, 32); + memcpy(signers[0].message, "jonas", 5); + memcpy(signers[1].message, "tim", 3); + memcpy(signers[2].message, "yannick", 7); +} + +/* Each signer generates their nonce pair (R1_i, R2_i) */ +static int nonce_generation_round(const secp256k1_context* ctx, + struct signer_secrets *signer_secrets, + struct signer *signers) { + int i; + for (i = 0; i < N_SIGNERS; i++) { + unsigned char seckey[32]; + unsigned char session_secrand[32]; + + /* Create random session ID. It is absolutely necessary that the session ID + * is unique for every call of secp256k1_fullagg_nonce_gen. Otherwise + * it's trivial for an attacker to extract the secret key! */ + if (!fill_random(session_secrand, sizeof(session_secrand))) { + return 0; + } + if (!secp256k1_keypair_sec(ctx, seckey, &signer_secrets[i].keypair)) { + return 0; + } + + /* Initialize session and create secret nonce for signing and public + * nonce to send to the coordinator. Each signer provides their own + * message here, which binds the nonce to their specific message. */ + if (!secp256k1_fullagg_nonce_gen(ctx, &signer_secrets[i].secnonce, &signers[i].pubnonce, + session_secrand, seckey, &signers[i].pubkey, + signers[i].message, NULL)) { + return 0; + } + + secure_erase(seckey, sizeof(seckey)); + } + return 1; +} + +/* Each signer creates their partial signature */ +static int partial_signing_round(const secp256k1_context* ctx, + struct signer_secrets *signer_secrets, + struct signer *signers, + const secp256k1_fullagg_session *session, + const secp256k1_pubkey **pubkeys, + const unsigned char **messages, + const secp256k1_fullagg_pubnonce **pubnonces) { + int i; + for (i = 0; i < N_SIGNERS; i++) { + /* Each signer computes their partial signature: + * - First computes their challenge c_i = H_sig(L, R, X_i, m_i) where L is the list of all (Xi, mi) pairs + * - Then computes s_i = r1_i + b*r2_i + c_i*x_i + * The partial_sign function will clear the secnonce by setting it to 0. That's because + * you must _never_ reuse the secnonce. If you do, you effectively reuse the nonce and + * leak the secret key. */ + if (!secp256k1_fullagg_partial_sign(ctx, &signers[i].partial_sig, + &signer_secrets[i].secnonce, + &signer_secrets[i].keypair, + session, pubkeys, messages, + pubnonces, i)) { + return 0; + } + } + return 1; +} + +/* Verify each partial signature individually */ +static int verify_partial_signatures(const secp256k1_context* ctx, + struct signer *signers, + const secp256k1_fullagg_session *session, + const secp256k1_pubkey **pubkeys, + const unsigned char **messages) { + int i; + /* The coordinator can optionally verify each partial signature individually + * before aggregating them. This helps identify which signer(s) may have + * produced invalid signatures if the aggregate signature fails to verify. */ + for (i = 0; i < N_SIGNERS; i++) { + if (!secp256k1_fullagg_partial_sig_verify(ctx, &signers[i].partial_sig, + &signers[i].pubnonce, + &signers[i].pubkey, + session, pubkeys, messages, i)) { + printf("Partial signature %d failed to verify\n", i); + return 0; + } + } + return 1; +} + +int main(void) { + secp256k1_context* ctx; + int i; + struct signer_secrets signer_secrets[N_SIGNERS]; + struct signer signers[N_SIGNERS]; + const secp256k1_pubkey *pubkeys[N_SIGNERS]; + const unsigned char *messages[N_SIGNERS]; + const secp256k1_fullagg_pubnonce *pubnonces[N_SIGNERS]; + const secp256k1_fullagg_partial_sig *partial_sigs[N_SIGNERS]; + secp256k1_fullagg_session session; + secp256k1_fullagg_aggnonce agg_pubnonce; + unsigned char sig[64]; + + ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + printf("\n=== Running FullAgg Example ===\n"); + printf("Creating a single signature for %d signers with different messages\n\n", N_SIGNERS); + + printf("Signers: Creating key pairs... "); + fflush(stdout); + for (i = 0; i < N_SIGNERS; i++) { + if (!create_keypair(ctx, &signer_secrets[i], &signers[i])) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + pubkeys[i] = &signers[i].pubkey; + } + printf("ok\n"); + + printf("Signers: Setting up messages... "); + fflush(stdout); + setup_messages(signers); + for (i = 0; i < N_SIGNERS; i++) { + messages[i] = signers[i].message; + } + printf("ok\n"); + + printf("Signers: Generating nonces... "); + fflush(stdout); + /* In FullAgg, we use two nonces per signer to prevent rogue-key attacks + * without requiring a proof of possession. */ + if (!nonce_generation_round(ctx, signer_secrets, signers)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + for (i = 0; i < N_SIGNERS; i++) { + pubnonces[i] = &signers[i].pubnonce; + } + printf("ok\n"); + + printf("Coordinator: Aggregating nonces... "); + fflush(stdout); + /* The coordinator (can be any party) collects all public nonces and + * aggregates them: R1 = sum(R1_i), R2 = sum(R2_i) */ + if (!secp256k1_fullagg_nonce_agg(ctx, &agg_pubnonce, pubnonces, N_SIGNERS)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + printf("All: Initializing session... "); + fflush(stdout); + /* The session computes: + * - The nonce coefficient b = H_non(R1, R2, all Xi, all mi, all R2_i) + * - The final nonce R = R1 + b*R2 + * This binds the final nonce to all signers' keys, messages, and individual nonces. */ + if (!secp256k1_fullagg_session_init(ctx, &session, &agg_pubnonce, + pubkeys, messages, pubnonces, N_SIGNERS)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + printf("Signers: Creating partial signatures... "); + fflush(stdout); + if (!partial_signing_round(ctx, signer_secrets, signers, &session, + pubkeys, messages, pubnonces)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + for (i = 0; i < N_SIGNERS; i++) { + partial_sigs[i] = &signers[i].partial_sig; + } + printf("ok\n"); + + printf("Coordinator: Verifying partial signatures... "); + fflush(stdout); + if (!verify_partial_signatures(ctx, signers, &session, pubkeys, messages)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + printf("Coordinator: Aggregating signatures... "); + fflush(stdout); + /* The final signature is (R, s) where s = sum(s_i) */ + if (!secp256k1_fullagg_partial_sig_agg(ctx, sig, &session, partial_sigs, N_SIGNERS)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + printf("All: Verifying aggregate signature... "); + fflush(stdout); + /* Verify the aggregate signature against all public keys and their messages. + * The verification checks that s*G = R + sum(c_i*X_i) where each c_i is + * computed from the specific message m_i that signer i signed. */ + if (!secp256k1_fullagg_verify(ctx, sig, pubkeys, messages, N_SIGNERS)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + /* Test that the signature is specific to these exact messages */ + printf("Testing message binding... "); + fflush(stdout); + + /* Modify one message to verify the signature is bound to specific messages */ + signers[1].message[0] ^= 0xFF; + + if (secp256k1_fullagg_verify(ctx, sig, pubkeys, messages, N_SIGNERS)) { + printf("FAILED (signature verified with modified message)\n"); + return EXIT_FAILURE; + } + + /* Restore the original message and verify it works again */ + signers[1].message[0] ^= 0xFF; + + if (!secp256k1_fullagg_verify(ctx, sig, pubkeys, messages, N_SIGNERS)) { + printf("FAILED (signature doesn't verify after restoring)\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + printf("\nFinal Aggregate Signature: "); + for (i = 0; i < 64; i++) { + printf("%02x", sig[i]); + if (i == 31) printf(" "); + } + printf("\n"); + + /* It's best practice to try to clear secrets from memory after using them. + * This is done because some bugs can allow an attacker to leak memory, for + * example through "out of bounds" array access (see Heartbleed), or the OS + * swapping them to disk. Hence, we overwrite secret key material with zeros. + * + * Here we are preventing these writes from being optimized out, as any good compiler + * will remove any writes that aren't used. */ + for (i = 0; i < N_SIGNERS; i++) { + secure_erase(&signer_secrets[i], sizeof(signer_secrets[i])); + } + secp256k1_context_destroy(ctx); + return EXIT_SUCCESS; +} From 39e36190e06bd8fe6088a0d9973890758b3bce3c Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 12 Sep 2025 17:59:55 +0200 Subject: [PATCH 5/7] fullagg: Add to CI --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++++++------------ ci/ci.sh | 3 ++- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9783251d65..06e798c681 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ env: EXTRAKEYS: 'no' SCHNORRSIG: 'no' MUSIG: 'no' + SCHNORRSIG_FULLAGG: 'no' ELLSWIFT: 'no' ### test options SECP256K1_TEST_ITERS: 64 @@ -84,14 +85,14 @@ jobs: matrix: configuration: - env_vars: { WIDEMUL: 'int64', RECOVERY: 'yes' } - - env_vars: { WIDEMUL: 'int64', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } + - env_vars: { WIDEMUL: 'int64', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } - env_vars: { WIDEMUL: 'int128' } - env_vars: { WIDEMUL: 'int128_struct', ELLSWIFT: 'yes' } - env_vars: { WIDEMUL: 'int128', RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } - - env_vars: { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes' } + - env_vars: { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } - env_vars: { WIDEMUL: 'int128', ASM: 'x86_64', ELLSWIFT: 'yes' } - - env_vars: { RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes' } - - env_vars: { CTIMETESTS: 'no', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', CPPFLAGS: '-DVERIFY' } + - env_vars: { RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } + - env_vars: { CTIMETESTS: 'no', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes', CPPFLAGS: '-DVERIFY' } - env_vars: { BUILD: 'distcheck', WITH_VALGRIND: 'no', CTIMETESTS: 'no', BENCH: 'no' } - env_vars: { CPPFLAGS: '-DDETERMINISTIC' } - env_vars: { CFLAGS: '-O0', CTIMETESTS: 'no' } @@ -140,6 +141,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CC: ${{ matrix.cc }} @@ -172,6 +175,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -212,6 +217,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -243,6 +250,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -285,6 +294,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -346,6 +357,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -385,6 +398,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -438,6 +453,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CC: 'clang' @@ -474,6 +491,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' CTIMETESTS: 'no' @@ -519,14 +538,14 @@ jobs: fail-fast: false matrix: env_vars: - - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } + - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } - { WIDEMUL: 'int128_struct', ECMULTGENKB: 2, ECMULTWINDOW: 4 } - - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } + - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } - { WIDEMUL: 'int128', RECOVERY: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes', CC: 'gcc' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CPPFLAGS: '-DVERIFY', CTIMETESTS: 'no' } - BUILD: 'distcheck' @@ -573,13 +592,13 @@ jobs: fail-fast: false matrix: env_vars: - - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } + - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } - { WIDEMUL: 'int128_struct', ECMULTGENPRECISION: 2, ECMULTWINDOW: 4 } - - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } + - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } - { WIDEMUL: 'int128', RECOVERY: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CPPFLAGS: '-DVERIFY' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes', CC: 'gcc' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', SCHNORRSIG_FULLAGG: 'yes', CPPFLAGS: '-DVERIFY' } - BUILD: 'distcheck' steps: @@ -703,6 +722,8 @@ jobs: RECOVERY: 'yes' EXTRAKEYS: 'yes' SCHNORRSIG: 'yes' + EXPERIMENTAL: 'yes' + SCHNORRSIG_FULLAGG: 'yes' MUSIG: 'yes' ELLSWIFT: 'yes' diff --git a/ci/ci.sh b/ci/ci.sh index 08e84efd4f..5ee46ca9c6 100755 --- a/ci/ci.sh +++ b/ci/ci.sh @@ -13,7 +13,7 @@ print_environment() { # does not rely on bash. for var in WERROR_CFLAGS MAKEFLAGS BUILD \ ECMULTWINDOW ECMULTGENKB ASM WIDEMUL WITH_VALGRIND EXTRAFLAGS \ - EXPERIMENTAL ECDH RECOVERY EXTRAKEYS MUSIG SCHNORRSIG ELLSWIFT \ + EXPERIMENTAL ECDH RECOVERY EXTRAKEYS MUSIG SCHNORRSIG ELLSWIFT SCHNORRSIG_FULLAGG\ SECP256K1_TEST_ITERS BENCH SECP256K1_BENCH_ITERS CTIMETESTS SYMBOL_CHECK \ EXAMPLES \ HOST WRAPPER_CMD \ @@ -80,6 +80,7 @@ esac --enable-module-extrakeys="$EXTRAKEYS" \ --enable-module-schnorrsig="$SCHNORRSIG" \ --enable-module-musig="$MUSIG" \ + --enable-module-schnorrsig-fullagg="$SCHNORRSIG_FULLAGG" \ --enable-examples="$EXAMPLES" \ --enable-ctime-tests="$CTIMETESTS" \ --with-valgrind="$WITH_VALGRIND" \ From bfd388f5a2a14e76677cc66f7a3ed00256981d1c Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 12 Sep 2025 18:11:20 +0200 Subject: [PATCH 6/7] fullagg: Add to build system --- CMakeLists.txt | 2 ++ Makefile.am | 15 +++++++++++ configure.ac | 64 +++++++++++++++++++++++++++++----------------- src/CMakeLists.txt | 13 ++++++++++ 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11dc3f6e53..c0e7c330e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ option(SECP256K1_ENABLE_MODULE_RECOVERY "Enable ECDSA pubkey recovery module." O option(SECP256K1_ENABLE_MODULE_EXTRAKEYS "Enable extrakeys module." ON) option(SECP256K1_ENABLE_MODULE_SCHNORRSIG "Enable schnorrsig module." ON) option(SECP256K1_ENABLE_MODULE_MUSIG "Enable musig module." ON) +option(SECP256K1_ENABLE_MODULE_SCHNORRSIG_FULLAGG "Enable schnorrsig full-aggregation module." OFF) option(SECP256K1_ENABLE_MODULE_ELLSWIFT "Enable ElligatorSwift module." ON) option(SECP256K1_USE_EXTERNAL_DEFAULT_CALLBACKS "Enable external default callback functions." OFF) @@ -284,6 +285,7 @@ message(" ECDSA pubkey recovery ............... ${SECP256K1_ENABLE_MODULE_RECOV message(" extrakeys ........................... ${SECP256K1_ENABLE_MODULE_EXTRAKEYS}") message(" schnorrsig .......................... ${SECP256K1_ENABLE_MODULE_SCHNORRSIG}") message(" musig ............................... ${SECP256K1_ENABLE_MODULE_MUSIG}") +message(" schnorrsig fullagg .................. ${SECP256K1_ENABLE_MODULE_SCHNORRSIG_FULLAGG}") message(" ElligatorSwift ...................... ${SECP256K1_ENABLE_MODULE_ELLSWIFT}") message("Parameters:") message(" ecmult window size .................. ${SECP256K1_ECMULT_WINDOW_SIZE}") diff --git a/Makefile.am b/Makefile.am index d511853b05..0761b5b4bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -207,6 +207,17 @@ musig_example_LDFLAGS += -lbcrypt endif TESTS += musig_example endif +if ENABLE_MODULE_SCHNORRSIG_FULLAGG +noinst_PROGRAMS += fullagg_example +fullagg_example_SOURCES = examples/fullagg.c +fullagg_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC +fullagg_example_LDADD = libsecp256k1.la +fullagg_example_LDFLAGS = -static +if BUILD_WINDOWS +fullagg_example_LDFLAGS += -lbcrypt +endif +TESTS += fullagg_example +endif endif ### Precomputed tables @@ -308,6 +319,10 @@ if ENABLE_MODULE_MUSIG include src/modules/musig/Makefile.am.include endif +if ENABLE_MODULE_SCHNORRSIG_FULLAGG +include src/modules/schnorrsig_fullagg/Makefile.am.include +endif + if ENABLE_MODULE_ELLSWIFT include src/modules/ellswift/Makefile.am.include endif diff --git a/configure.ac b/configure.ac index 2f156ddc25..0d315bcb30 100644 --- a/configure.ac +++ b/configure.ac @@ -187,6 +187,10 @@ AC_ARG_ENABLE(module_musig, AS_HELP_STRING([--enable-module-musig],[enable MuSig2 module [default=yes]]), [], [SECP_SET_DEFAULT([enable_module_musig], [yes], [yes])]) +AC_ARG_ENABLE(module_schnorrsig_fullagg, + AS_HELP_STRING([--enable-module-schnorrsig-fullagg],[enable schnorrsig full-aggregation module (experimental) [default=no]]), [], + [SECP_SET_DEFAULT([enable_module_schnorrsig_fullagg], [no], [yes])]) + AC_ARG_ENABLE(module_ellswift, AS_HELP_STRING([--enable-module-ellswift],[enable ElligatorSwift module [default=yes]]), [], [SECP_SET_DEFAULT([enable_module_ellswift], [yes], [yes])]) @@ -397,6 +401,14 @@ SECP_CFLAGS="$SECP_CFLAGS $WERROR_CFLAGS" # Processing must be done in a reverse topological sorting of the dependency graph # (dependent module first). +if test x"$enable_module_schnorrsig_fullagg" = x"yes"; then + if test x"$enable_module_schnorrsig" = x"no"; then + AC_MSG_ERROR([Module dependency error: You have disabled the schnorrsig module explicitly, but it is required by the schnorrsig fullagg module.]) + fi + enable_module_schnorrsig=yes + SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_SCHNORRSIG_FULLAGG=1" +fi + if test x"$enable_module_ellswift" = x"yes"; then SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_ELLSWIFT=1" fi @@ -441,6 +453,10 @@ if test x"$enable_experimental" = x"no"; then if test x"$set_asm" = x"arm32"; then AC_MSG_ERROR([ARM32 assembly is experimental. Use --enable-experimental to allow.]) fi + + if test x"$enable_module_schnorrsig_fullagg" = x"yes"; then + AC_MSG_ERROR([Schnorrsig Full-Aggregation module is experimental. Use --enable-experimental to allow.]) + fi fi ### @@ -460,6 +476,7 @@ AM_CONDITIONAL([ENABLE_MODULE_ECDH], [test x"$enable_module_ecdh" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_RECOVERY], [test x"$enable_module_recovery" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_EXTRAKEYS], [test x"$enable_module_extrakeys" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_SCHNORRSIG], [test x"$enable_module_schnorrsig" = x"yes"]) +AM_CONDITIONAL([ENABLE_MODULE_SCHNORRSIG_FULLAGG], [test x"$enable_module_schnorrsig_fullagg" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_MUSIG], [test x"$enable_module_musig" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_ELLSWIFT], [test x"$enable_module_ellswift" = x"yes"]) AM_CONDITIONAL([USE_EXTERNAL_ASM], [test x"$enable_external_asm" = x"yes"]) @@ -473,34 +490,35 @@ AC_OUTPUT echo echo "Build Options:" -echo " with external callbacks = $enable_external_default_callbacks" -echo " with benchmarks = $enable_benchmark" -echo " with tests = $enable_tests" -echo " with exhaustive tests = $enable_exhaustive_tests" -echo " with ctime tests = $enable_ctime_tests" -echo " with coverage = $enable_coverage" -echo " with examples = $enable_examples" -echo " module ecdh = $enable_module_ecdh" -echo " module recovery = $enable_module_recovery" -echo " module extrakeys = $enable_module_extrakeys" -echo " module schnorrsig = $enable_module_schnorrsig" -echo " module musig = $enable_module_musig" -echo " module ellswift = $enable_module_ellswift" +echo " with external callbacks = $enable_external_default_callbacks" +echo " with benchmarks = $enable_benchmark" +echo " with tests = $enable_tests" +echo " with exhaustive tests = $enable_exhaustive_tests" +echo " with ctime tests = $enable_ctime_tests" +echo " with coverage = $enable_coverage" +echo " with examples = $enable_examples" +echo " module ecdh = $enable_module_ecdh" +echo " module recovery = $enable_module_recovery" +echo " module extrakeys = $enable_module_extrakeys" +echo " module schnorrsig = $enable_module_schnorrsig" +echo " module schnorrsig-fullagg = $enable_module_schnorrsig_fullagg" +echo " module musig = $enable_module_musig" +echo " module ellswift = $enable_module_ellswift" echo -echo " asm = $set_asm" -echo " ecmult window size = $set_ecmult_window" -echo " ecmult gen table size = $set_ecmult_gen_kb KiB" +echo " asm = $set_asm" +echo " ecmult window size = $set_ecmult_window" +echo " ecmult gen table size = $set_ecmult_gen_kb KiB" # Hide test-only options unless they're used. if test x"$set_widemul" != xauto; then -echo " wide multiplication = $set_widemul" +echo " wide multiplication = $set_widemul" fi echo -echo " valgrind = $enable_valgrind" -echo " CC = $CC" -echo " CPPFLAGS = $CPPFLAGS" -echo " SECP_CFLAGS = $SECP_CFLAGS" -echo " CFLAGS = $CFLAGS" -echo " LDFLAGS = $LDFLAGS" +echo " valgrind = $enable_valgrind" +echo " CC = $CC" +echo " CPPFLAGS = $CPPFLAGS" +echo " SECP_CFLAGS = $SECP_CFLAGS" +echo " CFLAGS = $CFLAGS" +echo " LDFLAGS = $LDFLAGS" if test x"$print_msan_notice" = x"yes"; then echo diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fa3b2903eb..329b34ddb5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,19 @@ if(SECP256K1_ENABLE_MODULE_MUSIG) set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_musig.h) endif() +option(SECP256K1_EXPERIMENTAL "Allow experimental configuration options." OFF) +if(SECP256K1_ENABLE_MODULE_SCHNORRSIG_FULLAGG) + if(NOT SECP256K1_EXPERIMENTAL) + message(FATAL_ERROR "Schnorrsig full-aggregation is experimental. Use -DSECP256K1_EXPERIMENTAL=ON to allow.") + endif() + if(DEFINED SECP256K1_ENABLE_MODULE_SCHNORRSIG AND NOT SECP256K1_ENABLE_MODULE_SCHNORRSIG) + message(FATAL_ERROR "Module dependency error: You have disabled the schnorrsig module explicitly, but it is required by the Schnorrsig full-aggregation module.") + endif() + set(SECP256K1_ENABLE_MODULE_SCHNORRSIG ON) + add_compile_definitions(ENABLE_MODULE_SCHNORRSIG_FULLAGG=1) + set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_schnorrsig_fullagg.h) +endif() + if(SECP256K1_ENABLE_MODULE_SCHNORRSIG) if(DEFINED SECP256K1_ENABLE_MODULE_EXTRAKEYS AND NOT SECP256K1_ENABLE_MODULE_EXTRAKEYS) message(FATAL_ERROR "Module dependency error: You have disabled the extrakeys module explicitly, but it is required by the schnorrsig module.") From 13a34e4921bb123d74fa4974327ba2ee33654118 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 12 Sep 2025 18:11:34 +0200 Subject: [PATCH 7/7] fullagg: Add docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 90edae1a2c..7ee286feb5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Features: * Optional module for Schnorr signatures according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). * Optional module for ElligatorSwift key exchange according to [BIP-324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki). * Optional module for MuSig2 Schnorr multi-signatures according to [BIP-327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki). +* Optional module for Schnorr signatures full aggregation according to [TODO](). Implementation details ---------------------- @@ -150,6 +151,7 @@ Usage examples can be found in the [examples](examples) directory. To compile th * [Deriving a shared secret (ECDH) example](examples/ecdh.c) * [ElligatorSwift key exchange example](examples/ellswift.c) * [MuSig2 Schnorr multi-signatures example](examples/musig.c) + * [Schnorr signatures full aggregation example](examples/fullagg.c) To compile the examples, make sure the corresponding modules are enabled.