diff --git a/CMakeLists.txt b/CMakeLists.txt index f1fbb6e1..4b83facf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ option(SECP256K1_ENABLE_MODULE_MUSIG "Enable musig module." ON) option(SECP256K1_ENABLE_MODULE_FROST "Enable FROST module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_CHILLDKG "Enable ChillDKG module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_ICEBERG "Enable Iceberg threshold-MuSig module (experimental)." OFF) +option(SECP256K1_ENABLE_MODULE_PREFRACTAL "Enable Prefractal nested FROST+MuSig2 module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_ELLSWIFT "Enable ElligatorSwift module." ON) option(SECP256K1_ENABLE_MODULE_GENERATOR "Enable NUMS generator module." ON) @@ -303,6 +304,7 @@ message(" musig ............................... ${SECP256K1_ENABLE_MODULE_MUSIG message(" frost ............................... ${SECP256K1_ENABLE_MODULE_FROST}") message(" chilldkg ............................ ${SECP256K1_ENABLE_MODULE_CHILLDKG}") message(" iceberg ............................. ${SECP256K1_ENABLE_MODULE_ICEBERG}") +message(" prefractal .......................... ${SECP256K1_ENABLE_MODULE_PREFRACTAL}") message(" ElligatorSwift ...................... ${SECP256K1_ENABLE_MODULE_ELLSWIFT}") message(" generator ........................... ${SECP256K1_ENABLE_MODULE_GENERATOR}") message(" rangeproof .......................... ${SECP256K1_ENABLE_MODULE_RANGEPROOF}") diff --git a/Makefile.am b/Makefile.am index 4deb521b..b72f0879 100644 --- a/Makefile.am +++ b/Makefile.am @@ -396,6 +396,10 @@ if ENABLE_MODULE_CHILLDKG include src/modules/chilldkg/Makefile.am.include endif +if ENABLE_MODULE_PREFRACTAL +include src/modules/prefractal/Makefile.am.include +endif + if ENABLE_MODULE_ICEBERG include src/modules/iceberg/Makefile.am.include endif diff --git a/configure.ac b/configure.ac index 6dec492e..683c3e14 100644 --- a/configure.ac +++ b/configure.ac @@ -255,6 +255,11 @@ AC_ARG_ENABLE(module_iceberg, [], [SECP_SET_DEFAULT([enable_module_iceberg], [no], [yes])]) +AC_ARG_ENABLE(module_prefractal, + AS_HELP_STRING([--enable-module-prefractal],[enable Prefractal nested FROST+MuSig2 module (experimental)]), + [], + [SECP_SET_DEFAULT([enable_module_prefractal], [no], [yes])]) + # Test-only override of the (autodetected by the C code) "widemul" setting. # Legal values are: # * int64 (for [u]int64_t), @@ -525,6 +530,27 @@ if test x"$enable_module_ellswift" = x"yes"; then SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_ELLSWIFT=1" fi +# This block must stay ahead of the musig and frost blocks below. The +# enable_module_* assignments here are only observed by blocks that run after +# them, and frost defaults to "no": placing this at the iceberg block's +# position (further down) would leave -DENABLE_MODULE_FROST=1 unemitted while +# AM_CONDITIONAL still saw the mutation, which builds frost's sources into a +# library whose secp256k1.c never included them. +if test x"$enable_module_prefractal" = x"yes"; then + # musig defaults to yes, so "no" here really does mean the user disabled it. + if test x"$enable_module_musig" = x"no"; then + AC_MSG_ERROR([Module dependency error: You have disabled the musig module explicitly, but it is required by the prefractal module.]) + fi + # frost defaults to no, so the same test would reject every prefractal build. + # SECP_SET_DEFAULT only runs in AC_ARG_ENABLE's action-if-not-given branch, + # so telling an explicit --disable-module-frost from the default would mean + # changing frost's own declaration; enabling prefractal simply implies frost + # instead. The CMake block does the same, for the same reason. + enable_module_frost=yes + enable_module_musig=yes + SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_PREFRACTAL=1" +fi + if test x"$enable_module_musig" = 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 musig module.]) @@ -625,6 +651,9 @@ 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_prefractal" = x"yes"; then + AC_MSG_ERROR([Prefractal module is experimental. Use --enable-experimental to allow.]) + fi if test x"$enable_module_frost" = x"yes"; then AC_MSG_ERROR([FROST module is experimental. Use --enable-experimental to allow.]) fi @@ -674,6 +703,7 @@ AM_CONDITIONAL([ENABLE_MODULE_SCHNORRSIG_HALFAGG], [test x"$enable_module_schnor AM_CONDITIONAL([ENABLE_MODULE_FROST], [test x"$enable_module_frost" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_CHILLDKG], [test x"$enable_module_chilldkg" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_ICEBERG], [test x"$enable_module_iceberg" = x"yes"]) +AM_CONDITIONAL([ENABLE_MODULE_PREFRACTAL], [test x"$enable_module_prefractal" = x"yes"]) AM_CONDITIONAL([USE_REDUCED_SURJECTION_PROOF_SIZE], [test x"$use_reduced_surjection_proof_size" = x"yes"]) AM_CONDITIONAL([USE_EXTERNAL_ASM], [test x"$enable_external_asm" = x"yes"]) AM_CONDITIONAL([USE_ASM_ARM], [test x"$set_asm" = x"arm32"]) @@ -718,6 +748,7 @@ echo " module schnorrsig-halfagg = $enable_module_schnorrsig_halfagg" echo " module frost = $enable_module_frost" echo " module chilldkg = $enable_module_chilldkg" echo " module iceberg = $enable_module_iceberg" +echo " module prefractal = $enable_module_prefractal" echo echo " asm = $set_asm" echo " ecmult window size = $set_ecmult_window" diff --git a/include/secp256k1_prefractal.h b/include/secp256k1_prefractal.h new file mode 100644 index 00000000..89f21a68 --- /dev/null +++ b/include/secp256k1_prefractal.h @@ -0,0 +1,235 @@ +#ifndef SECP256K1_PREFRACTAL_H +#define SECP256K1_PREFRACTAL_H + +#include "secp256k1_frost.h" +#include "secp256k1_musig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** This module implements a nested FROST+MuSig2 signing scheme ("prefractal"), + * which lets a FROST t-of-n group occupy ONE participant slot of an ordinary + * MuSig2 (BIP 327) session. + * + * WARNING: EXPERIMENTAL. Neither the scheme nor this implementation has been + * reviewed by anyone outside the project, and should not be used to protect + * anything of value. + * + * The construction is the nested signing scheme of frosty-musig + * (https://github.com/jesseposner/frosty-musig). Each group member computes + * + * s_i = k1_i + b_frost*b_musig*k2_i + e*a*lambda_i*g*gacc*d_i + * + * where b_frost is this module's nonce-binding coefficient and b_musig, e, a, + * g and gacc all belong to the OUTER MuSig2 session. The group publishes one + * ordinary MuSig2 public nonce and one ordinary MuSig2 partial signature, so + * cosigners cannot tell a group is involved and need no support for it. + * + * DELIBERATE DEVIATIONS FROM BIP 445 (see doc/prefractal.md): + * + * 1. b_frost does NOT commit to the message. BIP 445's nonce coefficient + * hashes the message, but this module is built for protocols that publish + * nonces before the message exists. The OUTER coefficient b_musig does + * commit to the message and binds b_frost through it, exactly as the + * iceberg module does with its own Iceberg/noncecoef tag. + * + * 2. There is NO g_frost factor. Stock FROST normalizes its threshold public + * key to even Y (see g_times_gacc_parity in the frost module), because it + * produces a BIP 340 x-only signature. Here the threshold public key is an + * inner participant of the outer key aggregation and is used as a FULL + * point, so all key-side parity normalization happens once, at the + * aggregate level, using the OUTER keyagg cache. Applying the frost-level + * factor would break the relation for every group whose key has odd Y. + * + * 3. The FROST tweak cache must be the identity (tacc == 0, gacc_parity == 0). + * The channel protocols this module targets tweak only the outer aggregate + * key. Every entry point below checks this rather than silently ignoring a + * tweaked cache. + * + * NONCE HANDLING: the caller supplies secnonces produced by + * secp256k1_frost_nonce_gen. As always, a secnonce MUST be used for exactly + * one signature; reuse across two different messages leaks the secret share + * and nothing here can detect it. + * + * The round-two signer set must be EXACTLY the round-one set: the Lagrange + * coefficients and the aggregate nonce are both defined over the + * participating set, so a proper subset produces an invalid signature with no + * error raised. Pass the same ids array to every function below. + */ + +/** Aggregate the group members' public nonces and export the group's + * OUTER-wire nonce. + * + * pubnonce_out is an ordinary MuSig2 public nonce, (R1, b_frost*R2), which is + * what the group sends to its cosigners. aggnonce_out is the UNSCALED FROST + * aggregate nonce, which the members need later for partial signing; it is an + * internal value and must be given back to secp256k1_prefractal_sign and + * secp256k1_prefractal_partial_sig_verify unchanged. + * + * b_frost = tagged_hash("Prefractal/noncecoef", + * ser32(u) || sorted ser32 ids || aggnonce66 || + * cbytes_ext(thresh_pk)) + * + * Returns: 1 on success, 0 if a pubnonce could not be loaded, if the ids are + * invalid, or if either output nonce component is the point at + * infinity. A FROST aggregate nonce component may legitimately be + * infinity (BIP 445 NonceAgg), but a MuSig2 public nonce has no + * encoding for it, so such a session must be restarted with fresh + * nonces. + * Args: ctx: pointer to a context object + * Out: pubnonce_out: the group's MuSig2 public nonce + * aggnonce_out: the group's unscaled FROST aggregate nonce + * In: pubnonces: array of pointers to the members' public nonces + * ids: array of the members' identifiers + * n_signers: number of members (must match the array lengths, at + * least 1 and at most SECP256K1_FROST_MAX_PARTICIPANTS) + * thresh_pk: the group's (untweaked) threshold public key + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_nonce_agg( + const secp256k1_context *ctx, + secp256k1_musig_pubnonce *pubnonce_out, + secp256k1_frost_aggnonce *aggnonce_out, + const secp256k1_frost_pubnonce *const *pubnonces, + const uint32_t *ids, + size_t n_signers, + const secp256k1_pubkey *thresh_pk +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(7); + +/** Produce one group member's nested partial signature. + * + * Computes s_i = k1 + b_frost*b_musig*k2 + e*a*lambda_i*g*gacc*d_i, with both + * nonce scalars negated iff the OUTER final nonce has odd Y, and the key-side + * factor g*gacc taken from the OUTER keyagg cache. There is deliberately no + * g_frost factor (see the module notes above). + * + * The secnonce is wiped, so a second call with the same secnonce fails. The + * partial signature is self-verified before it is returned, as BIP 445 + * recommends. + * + * Returns: 1 on success, 0 on failure. Failure cases include: an invalidated + * or malformed secnonce, an invalid secret share, my_id not in ids, + * a secret share that does not match its pubshare, a non-identity + * tweak_cache, a nonce component at infinity, and a failed + * self-verification. + * Args: ctx: pointer to a context object + * Out: partial_sig: the member's partial signature + * In/Out: secnonce: the member's secret nonce, wiped by this call + * In: secshare32: the member's 32-byte secret share + * my_id: the member's identifier + * ids: array of the participating members' identifiers, + * the SAME array given to _nonce_agg + * pubshares: array of the members' public shares, in the order + * of ids, or NULL to skip the share/pubshare check + * (providing them is recommended) + * n_signers: number of members + * aggnonce: the unscaled FROST aggregate nonce from _nonce_agg + * thresh_pk: the group's (untweaked) threshold public key + * tweak_cache: the group's FROST tweak cache, which MUST be the + * identity + * keyagg_cache: the OUTER MuSig2 keyagg cache, already carrying any + * BIP 341 tweak + * cosigner_aggnonce: the aggregate of the NON-group participants' + * MuSig2 public nonces + * msg32: the 32-byte message being signed + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_sign( + const secp256k1_context *ctx, + secp256k1_frost_partial_sig *partial_sig, + secp256k1_frost_secnonce *secnonce, + const unsigned char *secshare32, + uint32_t my_id, + const uint32_t *ids, + const secp256k1_pubkey *pubshares, + size_t n_signers, + const secp256k1_frost_aggnonce *aggnonce, + const secp256k1_pubkey *thresh_pk, + const secp256k1_frost_tweak_cache *tweak_cache, + const secp256k1_musig_keyagg_cache *keyagg_cache, + const secp256k1_musig_aggnonce *cosigner_aggnonce, + const unsigned char *msg32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11) SECP256K1_ARG_NONNULL(12) SECP256K1_ARG_NONNULL(13) SECP256K1_ARG_NONNULL(14); + +/** Verify one group member's nested partial signature. + * + * Checks s_i*G == R1_i + b_frost*b_musig*R2_i + e*a*lambda_i*g*gacc*P_i, with + * the nonce points negated iff the OUTER final nonce has odd Y. The session is + * recomputed from the same parameters secp256k1_prefractal_sign takes, so the + * caller must pass exactly the same ids, aggnonce, keys and message. + * + * This is the identifiable-abort tool: when the aggregate signature fails, + * running this over each member's share names the one at fault. + * + * Returns: 1 if the partial signature is valid, 0 otherwise. + * Args: ctx: pointer to a context object + * In: partial_sig: the partial signature to verify + * pubnonce: the member's public nonce, as given to _nonce_agg + * pubshare: the member's public share + * my_id: the member's identifier + * ids: array of the participating members' identifiers + * n_signers: number of members + * aggnonce: the unscaled FROST aggregate nonce from _nonce_agg + * thresh_pk: the group's (untweaked) threshold public key + * tweak_cache: the group's FROST tweak cache, which MUST be the + * identity + * keyagg_cache: the OUTER MuSig2 keyagg cache + * cosigner_aggnonce: the aggregate of the NON-group participants' nonces + * msg32: the 32-byte message being signed + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_partial_sig_verify( + const secp256k1_context *ctx, + const secp256k1_frost_partial_sig *partial_sig, + const secp256k1_frost_pubnonce *pubnonce, + const secp256k1_pubkey *pubshare, + uint32_t my_id, + const uint32_t *ids, + size_t n_signers, + const secp256k1_frost_aggnonce *aggnonce, + const secp256k1_pubkey *thresh_pk, + const secp256k1_frost_tweak_cache *tweak_cache, + const secp256k1_musig_keyagg_cache *keyagg_cache, + const secp256k1_musig_aggnonce *cosigner_aggnonce, + const unsigned char *msg32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11) SECP256K1_ARG_NONNULL(12) SECP256K1_ARG_NONNULL(13); + +/** Sum the members' partial signatures into one ordinary MuSig2 partial + * signature. + * + * The result is ready for secp256k1_musig_partial_sig_agg alongside the + * cosigners' partial signatures. The sum is plain because the FROST tweak + * cache is required to be the identity: with a tweak there would be an + * additional e*g*tacc term to fold in, and this module does not support that. + * + * This does not check that the shares are the ones the members would have + * produced; secp256k1_prefractal_partial_sig_verify answers that, one share at + * a time. + * + * Returns: 1 on success, 0 if a partial signature could not be loaded or if + * tweak_cache is not the identity. + * Args: ctx: pointer to a context object + * Out: sig_out: the resulting MuSig2 partial signature + * error_index: if non-NULL and a partial signature fails to load, + * receives its index in partial_sigs + * In: partial_sigs: array of pointers to the members' partial signatures + * n_sigs: number of partial signatures (at least 1 and at most + * SECP256K1_FROST_MAX_PARTICIPANTS) + * tweak_cache: the group's FROST tweak cache, which MUST be the + * identity + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_partial_sig_agg( + const secp256k1_context *ctx, + secp256k1_musig_partial_sig *sig_out, + size_t *error_index, + const secp256k1_frost_partial_sig *const *partial_sigs, + size_t n_sigs, + const secp256k1_frost_tweak_cache *tweak_cache +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_PREFRACTAL_H */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f84bb377..25c428b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,32 @@ if(SECP256K1_ENABLE_MODULE_ELLSWIFT) set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_ellswift.h) endif() +# Must precede the musig and frost blocks below: the set() calls here are what +# force those modules on, and they are only observed by blocks that run after. +if(SECP256K1_ENABLE_MODULE_PREFRACTAL) + # musig defaults to ON, so the guard every other block in this file uses + # reads as "the user turned it off explicitly" and is meaningful here. + if(DEFINED SECP256K1_ENABLE_MODULE_MUSIG AND NOT SECP256K1_ENABLE_MODULE_MUSIG) + message(FATAL_ERROR "Module dependency error: You have disabled the musig module explicitly, but it is required by the prefractal module.") + endif() + # frost is the first default-OFF module anything depends on. The same guard + # cannot be used: option() always leaves the variable DEFINED, so for a + # default-OFF module "DEFINED AND NOT" is true by default and would reject + # every prefractal build. There is no way to tell an explicit -D...=OFF from + # the default once both are in the cache, so enabling prefractal simply + # implies frost. + set(SECP256K1_ENABLE_MODULE_FROST ON) + set(SECP256K1_ENABLE_MODULE_MUSIG ON) + # Also lift them into the parent scope so the top-level configuration + # summary, which runs after add_subdirectory(src), reports what was actually + # built. Without this a prefractal-only configure prints "frost OFF" while + # compiling frost in. + set(SECP256K1_ENABLE_MODULE_FROST ON PARENT_SCOPE) + set(SECP256K1_ENABLE_MODULE_MUSIG ON PARENT_SCOPE) + add_compile_definitions(ENABLE_MODULE_PREFRACTAL=1) + set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_prefractal.h) +endif() + if(SECP256K1_ENABLE_MODULE_ICEBERG) if(DEFINED SECP256K1_ENABLE_MODULE_MUSIG AND NOT SECP256K1_ENABLE_MODULE_MUSIG) message(FATAL_ERROR "Module dependency error: You have disabled the musig module explicitly, but it is required by the iceberg module.") diff --git a/src/modules/prefractal/Makefile.am.include b/src/modules/prefractal/Makefile.am.include new file mode 100644 index 00000000..c8b6ec82 --- /dev/null +++ b/src/modules/prefractal/Makefile.am.include @@ -0,0 +1,4 @@ +include_HEADERS += include/secp256k1_prefractal.h +noinst_HEADERS += src/modules/prefractal/main_impl.h +noinst_HEADERS += src/modules/prefractal/session_impl.h +noinst_HEADERS += src/modules/prefractal/tests_impl.h diff --git a/src/modules/prefractal/main_impl.h b/src/modules/prefractal/main_impl.h new file mode 100644 index 00000000..d2e2c665 --- /dev/null +++ b/src/modules/prefractal/main_impl.h @@ -0,0 +1,14 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_PREFRACTAL_MAIN_H +#define SECP256K1_MODULE_PREFRACTAL_MAIN_H + +/* One layer only. Everything this module does is a session computation over + * values the frost and musig modules already know how to load and save, so + * there is no keygen, no serialization and no state of its own here. */ +#include "session_impl.h" + +#endif diff --git a/src/modules/prefractal/session_impl.h b/src/modules/prefractal/session_impl.h new file mode 100644 index 00000000..daa295bc --- /dev/null +++ b/src/modules/prefractal/session_impl.h @@ -0,0 +1,557 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_PREFRACTAL_SESSION_IMPL_H +#define SECP256K1_MODULE_PREFRACTAL_SESSION_IMPL_H + +#include + +#include "../../../include/secp256k1_prefractal.h" + +/* This module is compiled into the same translation unit as frost and musig + * and is included after both, so it may use their static internals. It adds + * nothing to either: every value it needs is loaded through their existing + * helpers. */ +#include "../frost/keygen.h" +#include "../frost/session.h" +#include "../musig/keyagg.h" +#include "../musig/session.h" + +#include "../../group.h" +#include "../../hash.h" +#include "../../scalar.h" +#include "../../util.h" + +/* Initializes SHA256 with fixed midstate. This midstate was computed by + * applying SHA256 to SHA256("Prefractal/noncecoef")||SHA256("Prefractal/noncecoef"). */ +static void secp256k1_prefractal_noncecoef_sha256_tagged(secp256k1_sha256 *sha) { + static const uint32_t midstate[8] = { + 0x1d1f5957ul, 0x0c41e94ful, 0x0e1ec98ful, 0x70d8f48eul, + 0x8fb8bc46ul, 0x1eece984ul, 0x3b015f4bul, 0x443998c6ul + }; + secp256k1_sha256_initialize_midstate(sha, 64, midstate); +} + +/* b_frost = H_Prefractal/noncecoef(ser32(u) || sorted ser32 ids || aggnonce66 + * || cbytes_ext(thresh_pk)) + * + * The preimage is BIP 445's noncecoef preimage with the message dropped and + * the group key carried in full rather than x-only. Both changes are + * deliberate: + * + * - No message. The protocols this module serves publish the group's wire + * nonce before the transaction being signed exists, so a coefficient that + * hashed the message could not be computed in round one and rebuilt in round + * two. The outer b_musig does hash the message and multiplies this one, so + * the product still binds it. The iceberg module makes the same trade for the + * same reason. + * + * - Full point. The group key is an inner participant of the outer key + * aggregation and is used as a full point everywhere downstream, so the + * binding covers the point that is actually in play, including its Y parity. + * Iceberg's noncecoef hashes its group key the same way. + * + * Computed here and nowhere else: round one publishes the scaled nonce and + * rounds two and three rebuild it, and the three have to agree exactly or every + * signature the group produces is invalid. */ +static void secp256k1_prefractal_noncecoef(const secp256k1_context *ctx, secp256k1_scalar *b_frost, const secp256k1_ge *aggnonce_pts, const uint32_t *ids, size_t n_signers, const secp256k1_ge *thresh_pk) { + const secp256k1_hash_ctx *hash_ctx = secp256k1_get_hash_context(ctx); + uint32_t sorted_ids[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_ge pts[2], pk = *thresh_pk; + secp256k1_sha256 sha; + unsigned char aggnonce66[66]; + unsigned char buf[33]; + unsigned char out[32]; + size_t i; + + pts[0] = aggnonce_pts[0]; + pts[1] = aggnonce_pts[1]; + + secp256k1_prefractal_noncecoef_sha256_tagged(&sha); + secp256k1_write_be32(buf, (uint32_t)n_signers); + secp256k1_sha256_write(hash_ctx, &sha, buf, 4); + /* Sorting keeps the coefficient independent of the caller's ordering, as + * BIP 445 serialize_ids does. */ + secp256k1_frost_sort_ids(sorted_ids, ids, n_signers); + for (i = 0; i < n_signers; i++) { + secp256k1_write_be32(buf, sorted_ids[i]); + secp256k1_sha256_write(hash_ctx, &sha, buf, 4); + } + /* An aggregate nonce component at infinity is encoded as 33 zero bytes + * (BIP 445 cbytes_ext). It cannot survive to the wire, but it can reach + * this hash: the caller learns it is unusable from the infinity check in + * secp256k1_prefractal_nonce_agg, after this runs. */ + secp256k1_musig_ge_serialize_ext(&aggnonce66[0], &pts[0]); + secp256k1_musig_ge_serialize_ext(&aggnonce66[33], &pts[1]); + secp256k1_sha256_write(hash_ctx, &sha, aggnonce66, sizeof(aggnonce66)); + secp256k1_musig_ge_serialize_ext(buf, &pk); + secp256k1_sha256_write(hash_ctx, &sha, buf, 33); + secp256k1_sha256_finalize(hash_ctx, &sha, out); + secp256k1_scalar_set_b32(b_frost, out, NULL); +} + +/* The nonce pair the group publishes, (R1, b_frost*R2), from its unscaled + * aggregate. Only the second component is scaled. + * + * Returns 0 if either output is the point at infinity. A frost aggregate nonce + * component may legitimately be infinity, but a musig pubnonce has no encoding + * for one, so such a session is unusable and has to be restarted. Both + * components are checked: the first is passed through unscaled and can be + * infinity on its own if the members' first-column contributions cancel. + * + * Round one publishes this and the later rounds rebuild it, so it is written + * once rather than three times. */ +static int secp256k1_prefractal_publish_nonce(secp256k1_ge *out, const secp256k1_ge *pre, const secp256k1_scalar *b_frost) { + secp256k1_gej r2j, scaled; + + out[0] = pre[0]; + secp256k1_gej_set_ge(&r2j, &pre[1]); + secp256k1_ecmult(&scaled, &r2j, b_frost, NULL); + secp256k1_ge_set_gej(&out[1], &scaled); + return !secp256k1_ge_is_infinity(&out[0]) && !secp256k1_ge_is_infinity(&out[1]); +} + +/* The FROST tweak cache must be the identity: gacc == 1 and tacc == 0. + * + * With a tweak there would be an extra e*g*tacc term to fold into the + * aggregation, which this module does not implement. Checking is not a + * formality: a tweaked cache would otherwise be accepted and produce a + * signature that fails only at the very end, against the outer aggregate. + * + * Note this says nothing about g_frost. Stock frost's key-side factor is + * g*gacc where g is -1 for an odd-Y threshold key, so an identity cache does + * NOT imply a factor of 1 there. This module has no g_frost term at all, + * because the threshold key is used as a full point by the outer aggregation + * (see the module notes in the public header). */ +static int secp256k1_prefractal_tweak_cache_is_identity(const secp256k1_context *ctx, secp256k1_ge *thresh_pk_out, const secp256k1_frost_tweak_cache *tweak_cache) { + secp256k1_frost_tweak_cache_internal cache_i; + + if (!secp256k1_frost_tweak_cache_load(ctx, &cache_i, tweak_cache)) { + return 0; + } + if (cache_i.gacc_parity != 0) { + return 0; + } + if (!secp256k1_scalar_is_zero(&cache_i.tacc)) { + return 0; + } + if (thresh_pk_out != NULL) { + *thresh_pk_out = cache_i.thresh_pk; + } + return 1; +} + +/* Everything a member needs to turn its nonce and share into a partial + * signature, and everything a verifier needs to check one. + * + * b0b1 is the product of the outer nonce coefficient and this module's, which + * is the factor on the second nonce component. key_coef is e*a*g*gacc, the + * factor on lambda_i*d_i. fin_parity says whether the outer final nonce came + * out odd, which flips both nonce terms. + * + * The group's own wire nonce is rebuilt here from the aggregate rather than + * taken as an argument, so a member never signs against a nonce a coordinator + * chose for it: b_frost is a hash of exactly that aggregate, and a member that + * accepted three fabricated aggregates under one label would be answering three + * equations in its own secrets. + * + * This is the prefractal counterpart of secp256k1_iceberg_session_values, and + * deliberately NOT of secp256k1_frost_get_session_values: the latter computes a + * standalone FROST session, including the x-only normalization of the threshold + * key that must not happen here. */ +static int secp256k1_prefractal_session_values(const secp256k1_context *ctx, secp256k1_scalar *b0b1, secp256k1_scalar *key_coef, int *fin_parity, const secp256k1_frost_aggnonce *aggnonce, const uint32_t *ids, size_t n_signers, const secp256k1_pubkey *thresh_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const secp256k1_musig_aggnonce *cosigner_aggnonce, const unsigned char *msg32) { + secp256k1_keyagg_cache_internal cache_i; + secp256k1_ge group_pts[2], cosigner_pts[2], total[2], pk; + secp256k1_scalar b_frost, b_musig, a, e; + secp256k1_gej acc; + unsigned char agg_pk32[32], fin_nonce[32]; + int i; + + if (!secp256k1_keyagg_cache_load(ctx, &cache_i, keyagg_cache)) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &pk, thresh_pk)) { + return 0; + } + if (!secp256k1_frost_aggnonce_load(ctx, group_pts, aggnonce)) { + return 0; + } + if (!secp256k1_musig_aggnonce_load(ctx, cosigner_pts, cosigner_aggnonce)) { + return 0; + } + + /* Rebuild the group's published nonce with the same function round one + * published it with, then add the cosigners' to it. */ + secp256k1_prefractal_noncecoef(ctx, &b_frost, group_pts, ids, n_signers, &pk); + if (!secp256k1_prefractal_publish_nonce(total, group_pts, &b_frost)) { + return 0; + } + for (i = 0; i < 2; i++) { + secp256k1_gej_set_ge(&acc, &total[i]); + secp256k1_gej_add_ge_var(&acc, &acc, &cosigner_pts[i], NULL); + secp256k1_ge_set_gej(&total[i], &acc); + } + + secp256k1_fe_get_b32(agg_pk32, &cache_i.pk.x); + secp256k1_musig_nonce_process_internal(ctx, fin_parity, fin_nonce, &b_musig, total, agg_pk32, msg32); + secp256k1_schnorrsig_challenge(secp256k1_get_hash_context(ctx), &e, fin_nonce, msg32, 32, agg_pk32); + + secp256k1_scalar_mul(b0b1, &b_musig, &b_frost); + + /* The key coefficient carries the aggregation weight and the parity + * bookkeeping from BIP 340: e*a*g*gacc, where the sign flips if the + * AGGREGATE key is odd exactly once against the accumulated parity. The + * group key's own Y parity is deliberately not consulted: it is an inner + * participant of this aggregation, not the key the signature verifies + * against. */ + secp256k1_musig_keyaggcoef(secp256k1_get_hash_context(ctx), &a, &cache_i, &pk); + secp256k1_scalar_mul(key_coef, &e, &a); + if (secp256k1_fe_is_odd(&cache_i.pk.y) != cache_i.parity_acc) { + secp256k1_scalar_negate(key_coef, key_coef); + } + return 1; +} + +/* The verification equation, shared by the self-check inside + * secp256k1_prefractal_sign and by secp256k1_prefractal_partial_sig_verify so + * the two can never drift apart: + * + * s_i*G == +-(R1_i + b_frost*b_musig*R2_i) + e*a*g*gacc*lambda_i*P_i + * + * rearranged into a single comparison against infinity. The sign on the nonce + * term follows the OUTER final nonce's parity. */ +static int secp256k1_prefractal_verify_partial_sig(const secp256k1_scalar *s, const secp256k1_ge *nonce_pts, const secp256k1_ge *pubshare, const secp256k1_scalar *lambda, const secp256k1_scalar *b0b1, const secp256k1_scalar *key_coef, int fin_parity) { + secp256k1_scalar coef, s_neg; + secp256k1_gej rj, pkj, tmp; + + /* The nonce components of a pubnonce are never the point at infinity. */ + VERIFY_CHECK(!secp256k1_ge_is_infinity(&nonce_pts[0])); + VERIFY_CHECK(!secp256k1_ge_is_infinity(&nonce_pts[1])); + secp256k1_gej_set_ge(&rj, &nonce_pts[1]); + secp256k1_ecmult(&rj, &rj, b0b1, NULL); + secp256k1_gej_add_ge_var(&rj, &rj, &nonce_pts[0], NULL); + if (fin_parity) { + secp256k1_gej_neg(&rj, &rj); + } + + secp256k1_scalar_mul(&coef, key_coef, lambda); + secp256k1_scalar_negate(&s_neg, s); + secp256k1_gej_set_ge(&pkj, pubshare); + secp256k1_ecmult(&tmp, &pkj, &coef, &s_neg); + secp256k1_gej_add_var(&tmp, &tmp, &rj, NULL); + return secp256k1_gej_is_infinity(&tmp); +} + +int secp256k1_prefractal_nonce_agg(const secp256k1_context *ctx, secp256k1_musig_pubnonce *pubnonce_out, secp256k1_frost_aggnonce *aggnonce_out, const secp256k1_frost_pubnonce *const *pubnonces, const uint32_t *ids, size_t n_signers, const secp256k1_pubkey *thresh_pk) { + secp256k1_gej sumj[2]; + secp256k1_ge sum[2], published[2], pk; + secp256k1_scalar b_frost; + size_t i; + int j; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(pubnonce_out != NULL); + memset(pubnonce_out, 0, sizeof(*pubnonce_out)); + ARG_CHECK(aggnonce_out != NULL); + memset(aggnonce_out, 0, sizeof(*aggnonce_out)); + ARG_CHECK(pubnonces != NULL); + ARG_CHECK(ids != NULL); + ARG_CHECK(thresh_pk != NULL); + ARG_CHECK(n_signers >= 1 && n_signers <= SECP256K1_FROST_MAX_PARTICIPANTS); + for (i = 0; i < n_signers; i++) { + ARG_CHECK(pubnonces[i] != NULL); + } + /* The ids are what the coefficient commits to and what the Lagrange values + * are computed over, so a duplicate has to be refused here rather than + * producing a nonce nobody can sign against. */ + if (!secp256k1_frost_ids_are_valid(ids, n_signers)) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &pk, thresh_pk)) { + return 0; + } + + secp256k1_gej_set_infinity(&sumj[0]); + secp256k1_gej_set_infinity(&sumj[1]); + for (i = 0; i < n_signers; i++) { + secp256k1_ge pts[2]; + if (!secp256k1_frost_pubnonce_load(ctx, pts, pubnonces[i])) { + return 0; + } + for (j = 0; j < 2; j++) { + secp256k1_gej_add_ge_var(&sumj[j], &sumj[j], &pts[j], NULL); + } + } + /* Either column sum may be infinity here (BIP 445 NonceAgg); the aggregate + * nonce has an encoding for that, and the check that matters happens after + * scaling, below. */ + secp256k1_ge_set_all_gej_var(sum, sumj, 2); + + secp256k1_prefractal_noncecoef(ctx, &b_frost, sum, ids, n_signers, &pk); + if (!secp256k1_prefractal_publish_nonce(published, sum, &b_frost)) { + return 0; + } + + secp256k1_frost_aggnonce_save(aggnonce_out, sum); + secp256k1_musig_pubnonce_save(pubnonce_out, published); + return 1; +} + +/* The secrets secp256k1_prefractal_sign holds. d and s are not yet meaningful + * on its early error paths; clearing them there writes zeros over whatever the + * stack held, which is what those paths want anyway. */ +static void secp256k1_prefractal_sign_clear(secp256k1_scalar *k, secp256k1_scalar *d, secp256k1_scalar *lambda, secp256k1_scalar *s) { + secp256k1_scalar_clear(&k[0]); + secp256k1_scalar_clear(&k[1]); + secp256k1_scalar_clear(d); + secp256k1_scalar_clear(lambda); + secp256k1_scalar_clear(s); +} + +int secp256k1_prefractal_sign(const secp256k1_context *ctx, secp256k1_frost_partial_sig *partial_sig, secp256k1_frost_secnonce *secnonce, const unsigned char *secshare32, uint32_t my_id, const uint32_t *ids, const secp256k1_pubkey *pubshares, size_t n_signers, const secp256k1_frost_aggnonce *aggnonce, const secp256k1_pubkey *thresh_pk, const secp256k1_frost_tweak_cache *tweak_cache, const secp256k1_musig_keyagg_cache *keyagg_cache, const secp256k1_musig_aggnonce *cosigner_aggnonce, const unsigned char *msg32) { + secp256k1_scalar k[2], d, lambda, s, b0b1, key_coef, tmp; + secp256k1_gej nonce_ptj[2]; + secp256k1_ge nonce_pts[2], cache_pk, pk; + size_t my_index = n_signers; + size_t i; + int fin_parity; + int ret; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(secnonce != NULL); + /* Fails if the magic doesn't match or the nonce has been invalidated. */ + ret = secp256k1_frost_secnonce_load(ctx, k, secnonce); + /* Wipe the secnonce to prevent nonce reuse. This will cause subsequent + * calls of this function with the same secnonce to fail. */ + secp256k1_memzero_explicit(secnonce, sizeof(*secnonce)); + if (!ret) { + secp256k1_scalar_clear(&k[0]); + secp256k1_scalar_clear(&k[1]); + return 0; + } + + ARG_CHECK(partial_sig != NULL); + memset(partial_sig, 0, sizeof(*partial_sig)); + ARG_CHECK(secshare32 != NULL); + ARG_CHECK(ids != NULL); + ARG_CHECK(aggnonce != NULL); + ARG_CHECK(thresh_pk != NULL); + ARG_CHECK(tweak_cache != NULL); + ARG_CHECK(keyagg_cache != NULL); + ARG_CHECK(cosigner_aggnonce != NULL); + ARG_CHECK(msg32 != NULL); + ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx)); + + secp256k1_scalar_clear(&d); + secp256k1_scalar_clear(&lambda); + secp256k1_scalar_clear(&s); + + if (n_signers < 1 || n_signers > SECP256K1_FROST_MAX_PARTICIPANTS) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + /* The tweak cache is checked HERE and not only at aggregation, so the key + * the member signs under is tied to the cache that was validated. */ + if (!secp256k1_prefractal_tweak_cache_is_identity(ctx, &cache_pk, tweak_cache)) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + if (!secp256k1_pubkey_load(ctx, &pk, thresh_pk)) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + /* An identity cache carries the threshold key untouched, so this pins the + * key argument to the cache instead of letting the two disagree. */ + if (!secp256k1_ge_eq_var(&cache_pk, &pk)) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + + /* Compute the pubnonce points from the unnegated nonces for the + * self-verification below. k[0] != 0 and k[1] != 0 is guaranteed by + * secnonce_load, so the points are not the point at infinity. */ + secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &nonce_ptj[0], &k[0]); + secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &nonce_ptj[1], &k[1]); + secp256k1_ge_set_all_gej(nonce_pts, nonce_ptj, 2); + secp256k1_declassify(ctx, &nonce_pts, sizeof(nonce_pts)); + + /* The secret share must be nonzero and less than the curve order. We can + * declassify the result of the check because branching on it only leaks + * whether the provided secret share is a valid secret key, which is not + * secret. */ + { + int share_valid = secp256k1_scalar_set_b32_seckey(&d, secshare32); + secp256k1_declassify(ctx, &share_valid, sizeof(share_valid)); + if (!share_valid) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + } + + for (i = 0; i < n_signers; i++) { + if (ids[i] == my_id) { + my_index = i; + break; + } + } + if (my_index == n_signers) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + /* If the pubshares are known, the secret share must match the signer's + * pubshare (recommended by BIP 445). */ + if (pubshares != NULL) { + secp256k1_ge expected, mine; + secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &mine, &d); + secp256k1_declassify(ctx, &mine, sizeof(mine)); + if (!secp256k1_pubkey_load(ctx, &expected, &pubshares[my_index])) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + if (!secp256k1_ge_eq_var(&mine, &expected)) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + } + if (!secp256k1_frost_derive_interpolating_value(&lambda, ids, n_signers, my_id)) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + if (!secp256k1_prefractal_session_values(ctx, &b0b1, &key_coef, &fin_parity, aggnonce, ids, n_signers, thresh_pk, keyagg_cache, cosigner_aggnonce, msg32)) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + + /* BIP 340: if the OUTER final nonce came out odd, both nonce terms flip. + * + * There is deliberately NO key-side flip for the group key here. See the + * module notes in include/secp256k1_prefractal.h. */ + if (fin_parity) { + secp256k1_scalar_negate(&k[0], &k[0]); + secp256k1_scalar_negate(&k[1], &k[1]); + } + + /* s_i = k1 + b_frost*b_musig*k2 + e*a*g*gacc*lambda_i*d_i */ + secp256k1_scalar_mul(&s, &key_coef, &lambda); + secp256k1_scalar_mul(&s, &s, &d); + secp256k1_scalar_mul(&tmp, &b0b1, &k[1]); + secp256k1_scalar_add(&s, &s, &tmp); + secp256k1_scalar_add(&s, &s, &k[0]); + + /* Self-verify the partial signature, as recommended by BIP 445. This can + * only fail in case of an implementation bug or catastrophic hardware + * failure, so the result of the verification is not secret. The partial + * signature itself is declassified first: it is the public output of this + * function, and the verification below multiplies with it in variable + * time. */ + { + int verified; + secp256k1_ge mine; + secp256k1_declassify(ctx, &s, sizeof(s)); + secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &mine, &d); + secp256k1_declassify(ctx, &mine, sizeof(mine)); + verified = secp256k1_prefractal_verify_partial_sig(&s, nonce_pts, &mine, &lambda, &b0b1, &key_coef, fin_parity); + secp256k1_declassify(ctx, &verified, sizeof(verified)); + if (!verified) { + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + return 0; + } + } + + secp256k1_frost_partial_sig_save(partial_sig, &s); + secp256k1_prefractal_sign_clear(k, &d, &lambda, &s); + secp256k1_scalar_clear(&tmp); + return 1; +} + +int secp256k1_prefractal_partial_sig_verify(const secp256k1_context *ctx, const secp256k1_frost_partial_sig *partial_sig, const secp256k1_frost_pubnonce *pubnonce, const secp256k1_pubkey *pubshare, uint32_t my_id, const uint32_t *ids, size_t n_signers, const secp256k1_frost_aggnonce *aggnonce, const secp256k1_pubkey *thresh_pk, const secp256k1_frost_tweak_cache *tweak_cache, const secp256k1_musig_keyagg_cache *keyagg_cache, const secp256k1_musig_aggnonce *cosigner_aggnonce, const unsigned char *msg32) { + secp256k1_scalar b0b1, key_coef, lambda, s; + secp256k1_ge nonce_pts[2], share_pt, cache_pk, pk; + int fin_parity; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(partial_sig != NULL); + ARG_CHECK(pubnonce != NULL); + ARG_CHECK(pubshare != NULL); + ARG_CHECK(ids != NULL); + ARG_CHECK(aggnonce != NULL); + ARG_CHECK(thresh_pk != NULL); + ARG_CHECK(tweak_cache != NULL); + ARG_CHECK(keyagg_cache != NULL); + ARG_CHECK(cosigner_aggnonce != NULL); + ARG_CHECK(msg32 != NULL); + + if (n_signers < 1 || n_signers > SECP256K1_FROST_MAX_PARTICIPANTS) { + return 0; + } + /* Recomputed from the same parameters signing used, including this check, + * so a share made under a tweaked cache cannot be validated by a verifier + * that was handed one. */ + if (!secp256k1_prefractal_tweak_cache_is_identity(ctx, &cache_pk, tweak_cache)) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &pk, thresh_pk)) { + return 0; + } + if (!secp256k1_ge_eq_var(&cache_pk, &pk)) { + return 0; + } + if (!secp256k1_frost_partial_sig_load(ctx, &s, partial_sig)) { + return 0; + } + if (!secp256k1_frost_pubnonce_load(ctx, nonce_pts, pubnonce)) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &share_pt, pubshare)) { + return 0; + } + if (!secp256k1_frost_derive_interpolating_value(&lambda, ids, n_signers, my_id)) { + return 0; + } + if (!secp256k1_prefractal_session_values(ctx, &b0b1, &key_coef, &fin_parity, aggnonce, ids, n_signers, thresh_pk, keyagg_cache, cosigner_aggnonce, msg32)) { + return 0; + } + return secp256k1_prefractal_verify_partial_sig(&s, nonce_pts, &share_pt, &lambda, &b0b1, &key_coef, fin_parity); +} + +int secp256k1_prefractal_partial_sig_agg(const secp256k1_context *ctx, secp256k1_musig_partial_sig *sig_out, size_t *error_index, const secp256k1_frost_partial_sig *const *partial_sigs, size_t n_sigs, const secp256k1_frost_tweak_cache *tweak_cache) { + secp256k1_scalar s, term; + size_t i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(sig_out != NULL); + memset(sig_out, 0, sizeof(*sig_out)); + ARG_CHECK(partial_sigs != NULL); + ARG_CHECK(tweak_cache != NULL); + ARG_CHECK(n_sigs >= 1 && n_sigs <= SECP256K1_FROST_MAX_PARTICIPANTS); + for (i = 0; i < n_sigs; i++) { + ARG_CHECK(partial_sigs[i] != NULL); + } + + /* The sum below is plain only because there is no accumulated tweak to + * fold in. Checked again here, and not only in sign, because the shares and + * the cache can reach a coordinator from different places. */ + if (!secp256k1_prefractal_tweak_cache_is_identity(ctx, NULL, tweak_cache)) { + return 0; + } + + secp256k1_scalar_set_int(&s, 0); + for (i = 0; i < n_sigs; i++) { + if (!secp256k1_frost_partial_sig_load(ctx, &term, partial_sigs[i])) { + if (error_index != NULL) { + *error_index = i; + } + return 0; + } + secp256k1_scalar_add(&s, &s, &term); + } + /* Saved through the musig helper rather than copied: the two partial + * signature structs are the same size but carry different magics, so a + * struct copy would produce something musig_partial_sig_agg rejects. */ + secp256k1_musig_partial_sig_save(sig_out, &s); + return 1; +} + +#endif /* SECP256K1_MODULE_PREFRACTAL_SESSION_IMPL_H */ diff --git a/src/modules/prefractal/tests_impl.h b/src/modules/prefractal/tests_impl.h new file mode 100644 index 00000000..7a8026d8 --- /dev/null +++ b/src/modules/prefractal/tests_impl.h @@ -0,0 +1,14 @@ +#ifndef SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H +#define SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H + +#include "../../../include/secp256k1_prefractal.h" + +static void run_prefractal_smoke_test(void) { + CHECK(1); +} + +static const struct tf_test_entry tests_prefractal[] = { + CASE1(run_prefractal_smoke_test), +}; + +#endif /* SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H */ diff --git a/src/secp256k1.c b/src/secp256k1.c index d1b2f7c6..54b726e4 100644 --- a/src/secp256k1.c +++ b/src/secp256k1.c @@ -961,6 +961,10 @@ static int secp256k1_ge_parse_ext(secp256k1_ge* ge, const unsigned char *in33) { # include "modules/chilldkg/main_impl.h" #endif +#ifdef ENABLE_MODULE_PREFRACTAL +# include "modules/prefractal/main_impl.h" +#endif + #ifdef ENABLE_MODULE_ICEBERG # include "modules/iceberg/main_impl.h" #endif diff --git a/src/tests.c b/src/tests.c index 112c6f8d..61f94e91 100644 --- a/src/tests.c +++ b/src/tests.c @@ -7928,6 +7928,10 @@ static void run_ecdsa_wycheproof(void) { # include "modules/chilldkg/tests_impl.h" #endif +#ifdef ENABLE_MODULE_PREFRACTAL +# include "modules/prefractal/tests_impl.h" +#endif + #ifdef ENABLE_MODULE_ICEBERG # include "modules/iceberg/tests_impl.h" #endif @@ -8306,6 +8310,9 @@ static const struct tf_test_module registry_modules[] = { #ifdef ENABLE_MODULE_CHILLDKG MAKE_TEST_MODULE(chilldkg), #endif +#ifdef ENABLE_MODULE_PREFRACTAL + MAKE_TEST_MODULE(prefractal), +#endif #ifdef ENABLE_MODULE_ICEBERG MAKE_TEST_MODULE(iceberg), #endif