From 903da53c062ed4324d2e3c3c50cee2c9c5154ef1 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Fri, 4 Sep 2026 00:44:43 +0200 Subject: [PATCH] prefractal: add the nested FROST+MuSig2 module (API, implementation, wiring) Adds `prefractal`, an experimental module that lets a FROST t-of-n group occupy ONE participant slot of an ordinary MuSig2 (BIP 327) session. Each member computes s_i = k1_i + b_frost*b_musig*k2_i + e*a*lambda_i*g*gacc*d_i and the group publishes one ordinary MuSig2 public nonce and one ordinary MuSig2 partial signature, so cosigners need no support for it and cannot tell a group is involved. Four public functions, all sessionless (every call takes its session parameters explicitly, so there are no new opaque types, magics or *_SIZE constants to keep synchronised): secp256k1_prefractal_nonce_agg group wire nonce + unscaled aggnonce secp256k1_prefractal_sign one member's partial signature secp256k1_prefractal_partial_sig_verify identifiable abort secp256k1_prefractal_partial_sig_agg sum -> musig partial signature Three deliberate deviations from BIP 445, all documented in the public header: 1. b_frost does not commit to the message. The target protocols publish the group's wire nonce before the message exists, so a message-committing coefficient could not be computed in round one and rebuilt later. The outer b_musig does commit to the message and multiplies this one, so the product still binds it. Same trade the iceberg module makes, for the same reason. The preimage is BIP 445's with the message dropped and the group key carried in full rather than x-only, since it is used as a full point downstream. 2. There is NO g_frost factor. Stock FROST normalises its threshold key to even Y (g_times_gacc_parity = gacc_parity ^ pk_odd, frost/session_impl.h :664) because it produces a BIP 340 x-only signature. Here the threshold key is an inner participant of the outer key aggregation and is used as a full point, so all key-side parity normalisation happens once, at the aggregate level, off the OUTER keyagg cache. Note this is NOT implied by the tweak cache being the identity: with an identity cache g_frost is still -1 for every odd-Y group key, i.e. about half of them. Importing frost's key-side parity here would yield a signer that works for even-Y groups and fails for odd-Y ones. 3. The FROST tweak cache must be the identity (tacc == 0, gacc_parity == 0). Checked in sign and partial_sig_verify, not only in partial_sig_agg, so the key a member signs under is tied to the cache that was validated; sign and verify additionally require thresh_pk to equal the cache's own key so the two arguments cannot disagree. The verification equation lives in one helper used both by sign's BIP 445 self-check and by partial_sig_verify, so the two cannot drift apart. Build wiring. Three files order their module blocks differently and the constraints point in opposite directions: - src/secp256k1.c: the include goes AFTER frost and musig, because the module calls their static internals. - src/CMakeLists.txt: the block goes BEFORE both, because its set() calls are only observed by blocks that run later. - configure.ac: the block likewise goes before the musig block, NOT at iceberg's position further down. configure.ac orders musig and frost ahead of iceberg, and iceberg's late enable_module_musig=yes is harmless only because musig defaults to yes. frost defaults to no, so a late force-enable would leave -DENABLE_MODULE_FROST=1 unemitted while AM_CONDITIONAL still observed the mutation - a library whose secp256k1.c never included frost, built alongside frost's own sources. frost is also the first default-OFF module anything depends on, which breaks the dependency-guard idiom used everywhere else in both build systems: the existing "DEFINED X AND NOT X" (CMake) and "x$X = xno" (autotools) tests read as "the user disabled it explicitly" only for default-ON modules, and are true by default for a default-OFF one. Since neither build system can distinguish an explicit disable from the default once both are in the cache, enabling prefractal simply implies frost; the guard is kept for musig, where it still means what it says. The CMake block additionally lifts both dependencies into the parent scope so the top-level configuration summary reports what was actually built rather than printing "frost OFF" while compiling frost in. Verified on both build systems: cmake -B build -DSECP256K1_ENABLE_MODULE_PREFRACTAL=ON -DSECP256K1_BUILD_TESTS=ON -> musig/frost/prefractal all ON, tests pass, 4 prefractal symbols exported cmake -B build -DSECP256K1_BUILD_TESTS=ON -> prefractal OFF, default build unchanged, tests pass ./configure --enable-experimental --enable-module-prefractal && make && make check -> frost=yes forced on, -DENABLE_MODULE_FROST=1 emitted, 3/3 pass ./configure --enable-module-prefractal -> correctly refused: "Prefractal module is experimental" tests_impl.h is a placeholder here so the module links; the real suite lands next. --- CMakeLists.txt | 2 + Makefile.am | 4 + configure.ac | 31 ++ include/secp256k1_prefractal.h | 235 +++++++++ src/CMakeLists.txt | 26 + src/modules/prefractal/Makefile.am.include | 4 + src/modules/prefractal/main_impl.h | 14 + src/modules/prefractal/session_impl.h | 557 +++++++++++++++++++++ src/modules/prefractal/tests_impl.h | 14 + src/secp256k1.c | 4 + src/tests.c | 7 + 11 files changed, 898 insertions(+) create mode 100644 include/secp256k1_prefractal.h create mode 100644 src/modules/prefractal/Makefile.am.include create mode 100644 src/modules/prefractal/main_impl.h create mode 100644 src/modules/prefractal/session_impl.h create mode 100644 src/modules/prefractal/tests_impl.h 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