Merge branch 'prefractal'
Some checks failed
CI / Build arm64 Docker image (push) Has been cancelled
CI / Build x64 Docker image (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:ye… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALF… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJE… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITEL… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RE… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNOR… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:y… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes … (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAG… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECT… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIS… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECO… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRS… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:ye… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / i686: Linux (Debian stable) (clang --target=i686-pc-linux-gnu -isystem /usr/i686-linux-gnu/include, map[env_vars:map[]]) (push) Has been cancelled
CI / i686: Linux (Debian stable) (i686-linux-gnu-gcc, map[env_vars:map[]]) (push) Has been cancelled
CI / s390x (big-endian): Linux (Debian stable, QEMU) (map[env_vars:map[]]) (push) Has been cancelled
CI / ARM32: Linux (Debian stable, QEMU) (map[env_vars:map[ASM:arm32 EXPERIMENTAL:yes]]) (push) Has been cancelled
CI / ARM32: Linux (Debian stable, QEMU) (map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (clang, map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (gcc, map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[]]) (push) Has been cancelled
CI / ppc64le: Linux (Debian stable, QEMU) (map[env_vars:map[]]) (push) Has been cancelled
CI / Valgrind arm64 (memcheck) (push) Has been cancelled
CI / Valgrind i686 (memcheck) (push) Has been cancelled
CI / Valgrind x64 (memcheck) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:auto CC:clang]]) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:auto CC:i686-linux-gnu-gcc HOST:i686-linux-gnu]]) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:no CC:clang ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:no CC:i686-linux-gnu-gcc ECMULTGENKB:2 ECMULTWINDOW:2 HOST:i686-linux-gnu]]) (push) Has been cancelled
CI / MSan (clang, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -fsanitize-memory-param-retval -g CTIMETESTS:no]]) (push) Has been cancelled
CI / MSan (clang, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g -O3 CTIMETESTS:yes ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / MSan (clang, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g CTIMETESTS:yes]]) (push) Has been cancelled
CI / MSan (clang-snapshot, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -fsanitize-memory-param-retval -g CTIMETESTS:no]]) (push) Has been cancelled
CI / MSan (clang-snapshot, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g -O3 CTIMETESTS:yes ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / MSan (clang-snapshot, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g CTIMETESTS:yes]]) (push) Has been cancelled
CI / i686 (mingw32-w64): Windows (Debian stable, Wine) (push) Has been cancelled
CI / x86_64 (mingw32-w64): Windows (Debian stable, Wine) (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CC:gcc CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HA… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCH… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:y… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTION… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BUILD:distcheck]) (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[ECMULTGENKB:2 ECMULTWINDOW:4 WIDEMUL:int128_struct]) (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[RECOVERY:yes WIDEMUL:int128]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes CC:gcc ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIST:yes WID… (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes CPPFLAGS:-DVERIFY ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITEL… (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIST:yes WIDEMUL:in… (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIST:yes WIDEMUL:int128]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BUILD:distcheck]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[ECMULTGENKB:2 ECMULTWINDOW:4 WIDEMUL:int128_struct]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[RECOVERY:yes WIDEMUL:int128]) (push) Has been cancelled
CI / x86 (MSVC): Windows (VS 2022) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, static) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, shared) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, int128_struct with __(u)mulh) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, int128_struct) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, static) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, shared) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, int128_struct with __(u)mulh) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, int128_struct) (push) Has been cancelled
CI / x64 (MSVC): C++ (public headers) (push) Has been cancelled
CI / C++ -fpermissive (entire project) (map[env_vars:map[]]) (push) Has been cancelled
CI / C++ (public headers) (push) Has been cancelled
CI / SageMath prover (push) Has been cancelled
CI / release (push) Has been cancelled

Adds the prefractal module: a nested FROST+MuSig2 scheme letting a FROST t-of-n
group occupy one participant slot of an ordinary MuSig2 (BIP 327) session.

Three deliberate deviations from BIP 445, all documented in doc/prefractal.md:
the nonce coefficient does not commit to the message (so a group can publish
its wire nonce before the message exists); there is no g_frost factor, because
the threshold key is an inner participant of the outer aggregation and is used
as a full point; and the frost tweak cache must be the identity.

Merged the same way the iceberg module was.
This commit is contained in:
Kgothatso Ngako
2026-09-04 01:56:08 +02:00
13 changed files with 1791 additions and 0 deletions

View File

@@ -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}")

View File

@@ -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

View File

@@ -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"

265
doc/prefractal.md Normal file
View File

@@ -0,0 +1,265 @@
# Prefractal: a nested FROST+MuSig2 signer
**WARNING: EXPERIMENTAL.** Neither the scheme nor this implementation has been
reviewed by anyone outside the project. Do not use it to protect anything of
value. The construction comes from [frosty-musig][frosty], which is unaudited
research code, and it is built on this repository's `frost` module, which is
itself marked experimental and unstable.
[frosty]: https://github.com/jesseposner/frosty-musig
## What it does
It lets a FROST `t`-of-`n` group occupy **one participant slot** of an ordinary
MuSig2 (BIP 327) session. The group publishes one ordinary MuSig2 public nonce
and one ordinary MuSig2 partial signature. Cosigners need no support for any of
this and cannot tell a group is involved.
The motivating shape is a 2-of-2 taproot output where one of the two
"participants" is really a threshold group.
## The signing equation
Each member `i` of the participating set computes
```
s_i = k1_i + b_frost * b_musig * k2_i + e * a * lambda_i * g * gacc * d_i
```
- `k1_i`, `k2_i` — the member's two nonce scalars, both negated iff the OUTER
final nonce has odd Y.
- `b_frost` — this module's nonce-binding coefficient (below).
- `b_musig`, `e`, `a`, `g`, `gacc` — all from the OUTER MuSig2 session: the
nonce coefficient, the BIP 340 challenge over the aggregate key, the
key-aggregation coefficient of the group's threshold public key, and the
aggregate key's parity bookkeeping.
- `lambda_i` — the member's Lagrange interpolating value over the participating
set.
- `d_i` — the member's secret share.
The group's wire nonce is its FROST aggregate nonce with the second component
premultiplied by `b_frost`:
```
pubnonce = (R1, b_frost * R2)
```
The aggregator sums the members' shares. That is a plain sum, with no
interpolation, because `lambda_i` is already folded into each share.
## Three deliberate deviations from BIP 445
These are the parts a reviewer should look at hardest. Each one is a
considered trade, and each one is enforced or pinned somewhere in the code.
### 1. `b_frost` does not commit to the message
BIP 445's nonce coefficient hashes the message. This module's does not:
```
b_frost = tagged_hash("Prefractal/noncecoef",
ser32(u) || sorted ser32 ids || aggnonce66 ||
cbytes_ext(thresh_pk))
```
**Why.** The protocols this module targets publish the group's wire nonce
*before the message exists*. A lightning channel's funding signer publishes a
verification nonce at commitment number `N` long before the transaction that
nonce will sign has been built. A coefficient that hashed the message could not
be computed in round one and rebuilt identically in round two.
**Why it is not fatal.** The outer coefficient `b_musig` *does* commit to the
message, via `secp256k1_musig_nonce_process_internal`, and it multiplies
`b_frost` in every term where `b_frost` appears. The product binds the message.
This is the same trade the `iceberg` module makes, for the same reason, with
its own `Iceberg/noncecoef` tag.
**What is different from BIP 445's preimage.** The message is dropped, and the
threshold public key is hashed in its full 33-byte extended encoding rather
than x-only, because the key is used as a full point everywhere downstream (see
deviation 2) and the binding should cover the point that is actually in play.
**Pinned by.** `run_prefractal_midstate_test` checks the tagged-hash constant
against a freshly initialised one. Nothing else in the tree would notice a
changed `b_frost`; it would simply produce signatures that do not verify.
### 2. There is no `g_frost` factor
Stock FROST negates the secret share when the threshold public key has odd Y:
```c
/* frost/session_impl.h:664 */
session_i->g_times_gacc_parity = cache_i->gacc_parity ^ pk_odd;
/* frost/session_impl.h:797-800 */
if (session_i->g_times_gacc_parity) {
secp256k1_scalar_negate(&d, &d);
}
```
It does this because standalone FROST produces a BIP 340 x-only signature, so
the effective secret is normalised to the even-Y representative of the
threshold key.
**Here that must not happen.** The threshold public key is an *inner
participant* of the outer key aggregation. It enters `secp256k1_musig_pubkey_agg`
as a full 33-byte point, and MuSig2 does no per-participant parity
normalisation: the only key-side flip is at the aggregate level, off the OUTER
keyagg cache. So the group's members must reconstruct `d` with `d*G = thresh_pk`
exactly as dealt, whatever its Y parity.
**The trap.** It is tempting to say "the FROST tweak cache is the identity, so
the frost key-side factor is 1". That is **false**. The factor is `g * gacc`.
An identity cache gives `gacc = 1`, but `g` is still `-1` for every threshold
key with odd Y — roughly half of all groups. An implementation that reused
`secp256k1_frost_get_session_values`'s key-side handling would produce a signer
that works for even-Y groups and fails for odd-Y ones.
**Pinned by.** The test suite carries two *fixed* threshold secret keys, one of
each Y parity, and `run_prefractal_odd_y_group_key_test` asserts the parity of
its own fixture so it cannot quietly stop testing what it is named after. This
was verified by mutation: injecting the `pk_odd` negation makes the odd-Y test
fail while the even-Y one still passes. With a randomly seeded fixture that
would have been a coin flip per run.
### 3. The FROST tweak cache must be the identity
`tacc == 0` and `gacc_parity == 0`, checked by every entry point that takes a
cache.
**Why.** The target protocols tweak only the *outer* aggregate key — the BIP
341 key-path tweak is applied to the MuSig2 keyagg cache and handled by the
stock outer session. A frost-level tweak would add an `e * g * tacc` term that
the aggregator would have to fold in, and this module's aggregator is a plain
sum.
**Where it is checked.** In `secp256k1_prefractal_sign` and
`secp256k1_prefractal_partial_sig_verify`, not only in
`secp256k1_prefractal_partial_sig_agg`. Checking only at aggregation would be
too late and too weak: the signing path would never see the cache, so nothing
would tie the key a member signed under to the cache that was validated. `sign`
and `partial_sig_verify` additionally require `thresh_pk` to equal the cache's
own key, so the two arguments cannot disagree.
A tweak-aware aggregation variant (folding `e * g_musig * tacc`, as
frosty-musig's `nested_frost_partial_sig_agg` does) is a possible later
extension. It is not implemented.
## Rules the caller must follow
### One secnonce, one signature
The usual FROST rule, and this module cannot enforce it any better than FROST
can. `secp256k1_prefractal_sign` wipes the secnonce, so a second call with the
same one fails — including when the first call failed for some other reason,
which is why a member whose signing attempt was refused must generate a fresh
nonce rather than retry.
Deployments that derive nonces deterministically from a session label (which is
how a protocol gets a nonce it can publish early and rebuild later) inherit a
sharper version of the rule: **one label signs one message, group-wide**. Two
different messages under one label leak the secret share, and nothing raises an
error.
### The round-two signer set must equal the round-one set
Not a subset — the same set.
`lambda_i` and the aggregate nonce are both defined over the participating set.
If round one aggregates over `C` and only `S ⊂ C` signs, then the nonce terms
of `C \ S` are still in `R` while their key shares are absent from `sum(s_i)`,
and `sum_{i in S} lambda_i^C * d_i != d`. The result is an invalid signature
with no error raised at signing time.
This is worth stating explicitly because the `iceberg` module in this same
repository *does* tolerate a subset: its `2t-1` / `t` split comes from VSS
interpolation over the contributions, and FROST has no equivalent. Callers
porting between the two must not transpose the rule.
`run_prefractal_partial_sig_verify_test` covers the detectable half of this: a
share made for one signer set does not verify under another.
### Nonces at infinity
A FROST aggregate nonce component may legitimately be the point at infinity
(BIP 445 NonceAgg), but a MuSig2 public nonce has no encoding for one. Both
columns can reach infinity independently — the first is passed through
unscaled, the second only after the `b_frost` multiplication — and
`secp256k1_prefractal_nonce_agg` refuses both. Such a session has to be
restarted with fresh nonces.
## API
All four functions are sessionless: every call takes its session parameters
explicitly, so there are no opaque session objects, no new magics and no
`*_SIZE` constants to keep synchronised across bindings.
| Function | Role |
| --- | --- |
| `secp256k1_prefractal_nonce_agg` | round one: group wire nonce + unscaled aggnonce |
| `secp256k1_prefractal_sign` | round two: one member's partial signature |
| `secp256k1_prefractal_partial_sig_verify` | identifiable abort |
| `secp256k1_prefractal_partial_sig_agg` | sum shares into a MuSig2 partial signature |
`aggnonce_out` from `nonce_agg` is an internal value, not a wire value: it is
the *unscaled* FROST aggregate, and it must be handed back to `sign` and
`partial_sig_verify` unchanged. The wire value is `pubnonce_out`, an ordinary
66-byte MuSig2 public nonce.
Members generate their nonces with the stock `secp256k1_frost_nonce_gen`. This
module adds no nonce generation of its own.
## Relationship to the other modules
- **`frost`** stays pure, vector-pinned BIP 445. This module deliberately does
not live inside it: the deviations above are not BIP 445, and keeping them
behind their own opt-in flag gives auditors a clean scope boundary. It also
keeps `frost`'s dependency graph honest — `frost` depends only on
`schnorrsig`, and every pure-FROST consumer would otherwise have to build
`musig` too.
- **`musig`** is used unmodified, through its internals. Cosigners run stock
MuSig2 throughout.
- **`iceberg`** solves the same outer problem with a different inner scheme. The
two differ in ways that do not transfer: iceberg's quorum is `2t-1` in round
one and `t` in round two and it tolerates a round-two subset; prefractal uses
`t` in both rounds and requires set equality. Iceberg cannot express 2-of-2 or
3-of-4; prefractal can.
## Build
The module depends on both `frost` and `musig` and forces them on.
```
cmake -B build -DSECP256K1_ENABLE_MODULE_PREFRACTAL=ON -DSECP256K1_BUILD_TESTS=ON
cmake --build build && ./build/bin/tests --target=prefractal
```
```
./autogen.sh
./configure --enable-experimental --enable-module-prefractal
make && make check
```
Three files order their module blocks differently, and the constraints point in
opposite directions. Anyone adding a module by copying this one should read
this rather than copying `iceberg`'s positions:
- `src/secp256k1.c` — the include goes **after** `frost` and `musig`, because
the module calls their `static` internals and the whole library is one
translation unit.
- `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.
`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. Neither build system can distinguish an explicit
disable from the default once both are in the cache, so enabling `prefractal`
simply implies `frost`; the guard is kept for `musig`, where it still means what
it says.

View File

@@ -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 <stddef.h>
#include <stdint.h>
/** 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 */

View File

@@ -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.")

View File

@@ -61,6 +61,10 @@
#include "../include/secp256k1_chilldkg.h"
#endif
#ifdef ENABLE_MODULE_PREFRACTAL
#include "../include/secp256k1_prefractal.h"
#endif
#ifdef ENABLE_MODULE_ICEBERG
#include "../include/secp256k1_iceberg.h"
#include "../include/secp256k1_iceberg_dealer.h"
@@ -593,6 +597,100 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) {
}
#endif
#ifdef ENABLE_MODULE_PREFRACTAL
{
/* A 2-of-2 nested group with one stock musig cosigner, taken as far as
* one signature share. Secret here is the threshold key, the secret
* shares derived from it, and the session randomness the nonces come
* from. Not secret: the identifiers, the public shares, the threshold
* public key, both aggregate nonces, the outer keyagg cache, b_frost,
* and the resulting partial signature. */
unsigned char thresh_seckey[32];
unsigned char secshares[2 * 32];
unsigned char session_secrand[2][32];
unsigned char cosigner_seckey[32];
secp256k1_pubkey thresh_pk, pubshares[2], cosigner_pk;
const secp256k1_pubkey *outer_pubkeys[2];
uint32_t pf_ids[2] = { 0, 1 };
secp256k1_frost_tweak_cache pf_cache;
secp256k1_frost_secnonce pf_secnonce[2];
secp256k1_frost_pubnonce pf_pubnonce[2];
const secp256k1_frost_pubnonce *pf_pubnonce_ptrs[2];
secp256k1_frost_aggnonce pf_aggnonce;
secp256k1_frost_partial_sig pf_partial_sig;
secp256k1_xonly_pubkey outer_xonly;
secp256k1_musig_keyagg_cache outer_cache;
secp256k1_musig_pubnonce group_pubnonce, cosigner_pubnonce;
const secp256k1_musig_pubnonce *just_cosigner[1];
secp256k1_musig_secnonce cosigner_secnonce;
secp256k1_musig_aggnonce cosigner_aggnonce;
unsigned char cosigner_secrand[32];
pf_pubnonce_ptrs[0] = &pf_pubnonce[0];
pf_pubnonce_ptrs[1] = &pf_pubnonce[1];
SECP256K1_CHECKMEM_DEFINE(key, 32);
memcpy(thresh_seckey, key, sizeof(thresh_seckey));
thresh_seckey[0] = thresh_seckey[0] + 4;
memcpy(cosigner_seckey, key, sizeof(cosigner_seckey));
cosigner_seckey[0] = cosigner_seckey[0] + 5;
memcpy(session_secrand[0], key, 32);
session_secrand[0][0] = session_secrand[0][0] + 6;
memcpy(session_secrand[1], key, 32);
session_secrand[1][0] = session_secrand[1][0] + 7;
memcpy(cosigner_secrand, key, 32);
cosigner_secrand[0] = cosigner_secrand[0] + 8;
SECP256K1_CHECKMEM_DEFINE(msg, sizeof(msg));
SECP256K1_CHECKMEM_UNDEFINE(thresh_seckey, sizeof(thresh_seckey));
ret = secp256k1_frost_trusted_dealer_keygen(ctx, secshares, &thresh_pk, pubshares, 2, 2, thresh_seckey);
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
CHECK(ret == 1);
SECP256K1_CHECKMEM_DEFINE(&thresh_pk, sizeof(thresh_pk));
SECP256K1_CHECKMEM_DEFINE(pubshares, sizeof(pubshares));
CHECK(secp256k1_frost_tweak_cache_init(ctx, &pf_cache, &thresh_pk) == 1);
/* The outer aggregation and the cosigner's round one are entirely
* public as far as this module is concerned. */
SECP256K1_CHECKMEM_UNDEFINE(cosigner_seckey, sizeof(cosigner_seckey));
ret = secp256k1_ec_pubkey_create(ctx, &cosigner_pk, cosigner_seckey);
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
CHECK(ret == 1);
SECP256K1_CHECKMEM_DEFINE(&cosigner_pk, sizeof(cosigner_pk));
outer_pubkeys[0] = &thresh_pk;
outer_pubkeys[1] = &cosigner_pk;
CHECK(secp256k1_musig_pubkey_agg(ctx, &outer_xonly, &outer_cache, outer_pubkeys, 2) == 1);
SECP256K1_CHECKMEM_UNDEFINE(cosigner_secrand, sizeof(cosigner_secrand));
ret = secp256k1_musig_nonce_gen(ctx, &cosigner_secnonce, &cosigner_pubnonce, cosigner_secrand, cosigner_seckey, &cosigner_pk, msg, &outer_cache, NULL);
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
CHECK(ret == 1);
SECP256K1_CHECKMEM_DEFINE(&cosigner_pubnonce, sizeof(cosigner_pubnonce));
just_cosigner[0] = &cosigner_pubnonce;
CHECK(secp256k1_musig_nonce_agg(ctx, &cosigner_aggnonce, just_cosigner, 1) == 1);
/* Group round one. msg is NULL: the wire nonce is published before the
* message is known. */
for (i = 0; i < 2; i++) {
SECP256K1_CHECKMEM_UNDEFINE(session_secrand[i], 32);
SECP256K1_CHECKMEM_UNDEFINE(&secshares[32 * i], 32);
ret = secp256k1_frost_nonce_gen(ctx, &pf_secnonce[i], &pf_pubnonce[i], session_secrand[i], &secshares[32 * i], &pubshares[i], NULL, NULL, 0, NULL, 0);
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
CHECK(ret == 1);
SECP256K1_CHECKMEM_DEFINE(&pf_pubnonce[i], sizeof(pf_pubnonce[i]));
}
/* Aggregation is over published nonces only, so it is public. */
CHECK(secp256k1_prefractal_nonce_agg(ctx, &group_pubnonce, &pf_aggnonce, pf_pubnonce_ptrs, pf_ids, 2, &thresh_pk) == 1);
/* The share and the secnonce are secret; the partial signature is the
* public output. */
ret = secp256k1_prefractal_sign(ctx, &pf_partial_sig, &pf_secnonce[0], &secshares[0], pf_ids[0], pf_ids, pubshares, 2, &pf_aggnonce, &thresh_pk, &pf_cache, &outer_cache, &cosigner_aggnonce, msg);
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
CHECK(ret == 1);
SECP256K1_CHECKMEM_DEFINE(&pf_partial_sig, sizeof(pf_partial_sig));
}
#endif
#ifdef ENABLE_MODULE_ICEBERG
{
/* A 3-of-5 group, dealt from `key` and taken as far as one signature

View File

@@ -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

View File

@@ -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

View File

@@ -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 <string.h>
#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 */

View File

@@ -0,0 +1,544 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H
#define SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H
#include "../../../include/secp256k1_prefractal.h"
/* Two FIXED threshold secret keys, one of each Y parity of the resulting
* threshold public key.
*
* The odd one is the point of this pair. The nested equation has no g_frost
* factor, and the reason is NOT that the tweak cache is the identity: stock
* frost's key-side factor is g*gacc with g = -1 for an odd-Y threshold key, so
* an implementation that imported frost's key-side parity would work for
* even-Y groups and fail for odd-Y ones. A randomly seeded fixture makes that
* a coin flip per run, so both parities are pinned here instead. */
static const unsigned char prefractal_test_seckey_even[32] = {
0x44, 0xa2, 0x82, 0x5e, 0x46, 0x26, 0xfa, 0x53,
0xf5, 0x2c, 0x2e, 0x6a, 0x40, 0x7a, 0xfc, 0xb9,
0xb7, 0xe8, 0x7d, 0x63, 0x30, 0x6b, 0x0d, 0x69,
0xae, 0x3d, 0x0d, 0x29, 0xeb, 0x6c, 0xa6, 0x08
};
static const unsigned char prefractal_test_seckey_odd[32] = {
0xd3, 0x27, 0x59, 0x3f, 0xe7, 0x53, 0xf6, 0xfd,
0xe3, 0x8f, 0x29, 0xfd, 0x26, 0x39, 0xd4, 0x4f,
0x62, 0x05, 0x4b, 0xab, 0xea, 0x21, 0xa3, 0x59,
0xa4, 0x1d, 0x65, 0x1c, 0x81, 0xf1, 0xe0, 0x1e
};
/* Everything one nested session needs, so the tests below can set one up in a
* line and then poke at individual pieces. */
typedef struct {
unsigned int n, t;
unsigned char secshares[SECP256K1_FROST_MAX_PARTICIPANTS][32];
secp256k1_pubkey pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
secp256k1_pubkey thresh_pk;
secp256k1_frost_tweak_cache tweak_cache;
uint32_t ids[SECP256K1_FROST_MAX_PARTICIPANTS];
secp256k1_frost_secnonce secnonces[SECP256K1_FROST_MAX_PARTICIPANTS];
secp256k1_frost_pubnonce pubnonces[SECP256K1_FROST_MAX_PARTICIPANTS];
const secp256k1_frost_pubnonce *pubnonce_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS];
secp256k1_frost_aggnonce aggnonce;
secp256k1_musig_pubnonce group_pubnonce;
secp256k1_keypair cosigner_keypair;
secp256k1_pubkey cosigner_pk;
secp256k1_xonly_pubkey agg_xonly;
secp256k1_musig_keyagg_cache keyagg_cache;
secp256k1_musig_secnonce cosigner_secnonce;
secp256k1_musig_pubnonce cosigner_pubnonce;
secp256k1_musig_aggnonce cosigner_aggnonce;
secp256k1_musig_aggnonce full_aggnonce;
unsigned char msg[32];
} prefractal_test_session;
/* Deal a group, build the outer 2-key aggregation with one stock musig
* cosigner, and run round one on both sides.
*
* group_first picks which side of the outer aggregation the group key sits on.
* BIP 327 KeyAgg gives the "second" distinct key a coefficient of exactly 1
* and hashes every other one, so the two orders exercise genuinely different
* arithmetic for the group; both are run everywhere below.
*
* xonly_tweak applies the BIP 341 style tweak to the OUTER cache only, which
* is the arrangement the channel protocols use. */
static void prefractal_test_setup(prefractal_test_session *s, unsigned int n, unsigned int t,
const unsigned char *thresh_seckey, int group_first,
const unsigned char *xonly_tweak) {
const secp256k1_pubkey *pubkeys[2];
const secp256k1_musig_pubnonce *just_cosigner[1];
unsigned char cosigner_seckey[32], secrand[32], thresh_pk32[32];
secp256k1_xonly_pubkey thresh_xonly;
unsigned int k;
s->n = n;
s->t = t;
testrand256(s->msg);
testrand256(cosigner_seckey);
CHECK(secp256k1_frost_trusted_dealer_keygen(CTX, s->secshares[0], &s->thresh_pk,
s->pubshares, n, t, thresh_seckey) == 1);
CHECK(secp256k1_frost_tweak_cache_init(CTX, &s->tweak_cache, &s->thresh_pk) == 1);
for (k = 0; k < n; k++) {
s->ids[k] = k;
}
/* The group's key enters the outer aggregation as an ordinary public key,
* in full. */
CHECK(secp256k1_keypair_create(CTX, &s->cosigner_keypair, cosigner_seckey) == 1);
CHECK(secp256k1_keypair_pub(CTX, &s->cosigner_pk, &s->cosigner_keypair) == 1);
pubkeys[group_first ? 0 : 1] = &s->thresh_pk;
pubkeys[group_first ? 1 : 0] = &s->cosigner_pk;
CHECK(secp256k1_musig_pubkey_agg(CTX, &s->agg_xonly, &s->keyagg_cache, pubkeys, 2) == 1);
if (xonly_tweak != NULL) {
secp256k1_pubkey tweaked;
CHECK(secp256k1_musig_pubkey_xonly_tweak_add(CTX, &tweaked, &s->keyagg_cache, xonly_tweak) == 1);
CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &s->agg_xonly, NULL, &tweaked) == 1);
}
/* Round one, cosigner side. */
testrand256(secrand);
CHECK(secp256k1_musig_nonce_gen(CTX, &s->cosigner_secnonce, &s->cosigner_pubnonce, secrand,
cosigner_seckey, &s->cosigner_pk, s->msg, &s->keyagg_cache, NULL) == 1);
just_cosigner[0] = &s->cosigner_pubnonce;
CHECK(secp256k1_musig_nonce_agg(CTX, &s->cosigner_aggnonce, just_cosigner, 1) == 1);
/* Round one, group side. The nonces are generated the way the module
* expects them: msg = NULL, because the wire nonce is published before the
* message exists. */
CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &thresh_xonly, NULL, &s->thresh_pk) == 1);
CHECK(secp256k1_xonly_pubkey_serialize(CTX, thresh_pk32, &thresh_xonly) == 1);
for (k = 0; k < t; k++) {
testrand256(secrand);
CHECK(secp256k1_frost_nonce_gen(CTX, &s->secnonces[k], &s->pubnonces[k], secrand,
s->secshares[k], &s->pubshares[k], thresh_pk32,
NULL, 0, NULL, 0) == 1);
s->pubnonce_ptrs[k] = &s->pubnonces[k];
}
CHECK(secp256k1_prefractal_nonce_agg(CTX, &s->group_pubnonce, &s->aggnonce,
s->pubnonce_ptrs, s->ids, t, &s->thresh_pk) == 1);
{
const secp256k1_musig_pubnonce *all[2];
all[group_first ? 0 : 1] = &s->group_pubnonce;
all[group_first ? 1 : 0] = &s->cosigner_pubnonce;
CHECK(secp256k1_musig_nonce_agg(CTX, &s->full_aggnonce, all, 2) == 1);
}
}
/* Round two on both sides, ending in a BIP 340 signature over the outer
* aggregate key. Returns what secp256k1_schnorrsig_verify says about it. */
static int prefractal_test_finish(prefractal_test_session *s, int group_first, unsigned char *sig64) {
secp256k1_frost_partial_sig psigs[SECP256K1_FROST_MAX_PARTICIPANTS];
const secp256k1_frost_partial_sig *psig_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS];
const secp256k1_musig_partial_sig *musig_psigs[2];
secp256k1_musig_partial_sig group_psig, cosigner_psig;
secp256k1_musig_session session;
unsigned int k;
CHECK(secp256k1_musig_nonce_process(CTX, &session, &s->full_aggnonce, s->msg, &s->keyagg_cache, NULL) == 1);
CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &s->cosigner_secnonce,
&s->cosigner_keypair, &s->keyagg_cache, &session) == 1);
for (k = 0; k < s->t; k++) {
CHECK(secp256k1_prefractal_sign(CTX, &psigs[k], &s->secnonces[k], s->secshares[k],
s->ids[k], s->ids, s->pubshares, s->t, &s->aggnonce,
&s->thresh_pk, &s->tweak_cache, &s->keyagg_cache,
&s->cosigner_aggnonce, s->msg) == 1);
psig_ptrs[k] = &psigs[k];
/* Every share verifies against its author's public share. */
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psigs[k], &s->pubnonces[k],
&s->pubshares[k], s->ids[k], s->ids, s->t,
&s->aggnonce, &s->thresh_pk, &s->tweak_cache,
&s->keyagg_cache, &s->cosigner_aggnonce,
s->msg) == 1);
}
CHECK(secp256k1_prefractal_partial_sig_agg(CTX, &group_psig, NULL, psig_ptrs, s->t,
&s->tweak_cache) == 1);
musig_psigs[group_first ? 0 : 1] = &group_psig;
musig_psigs[group_first ? 1 : 0] = &cosigner_psig;
CHECK(secp256k1_musig_partial_sig_agg(CTX, sig64, &session, musig_psigs, 2) == 1);
return secp256k1_schnorrsig_verify(CTX, sig64, s->msg, 32, &s->agg_xonly);
}
/* The round trip, over every t-of-n this module claims to support, both
* lexicographic positions of the group key, both group-key Y parities, and
* with and without the outer BIP 341 tweak. */
static void run_prefractal_e2e_test(void) {
static const unsigned char configs[][2] = { {2,2}, {3,2}, {5,3}, {4,3}, {7,5} };
size_t config;
int group_first, odd, tweaked;
for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) {
for (group_first = 0; group_first <= 1; group_first++) {
for (odd = 0; odd <= 1; odd++) {
for (tweaked = 0; tweaked <= 1; tweaked++) {
prefractal_test_session s;
unsigned char sig[64], tweak[32];
testrand256(tweak);
prefractal_test_setup(&s, configs[config][0], configs[config][1],
odd ? prefractal_test_seckey_odd : prefractal_test_seckey_even,
group_first, tweaked ? tweak : NULL);
CHECK(prefractal_test_finish(&s, group_first, sig) == 1);
}
}
}
}
}
/* The odd-Y case on its own, so a failure names the cause rather than showing
* up as one iteration of the matrix above. If the g_frost factor were imported
* from stock frost, this test would fail and the even-Y one would pass. */
static void run_prefractal_odd_y_group_key_test(void) {
prefractal_test_session s;
secp256k1_xonly_pubkey xonly;
unsigned char sig[64], ser[32];
int parity = -1;
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL);
/* Confirm the fixture really is odd-Y, so the test cannot quietly stop
* testing what it is named after. */
CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &xonly, &parity, &s.thresh_pk) == 1);
CHECK(secp256k1_xonly_pubkey_serialize(CTX, ser, &xonly) == 1);
CHECK(parity == 1);
CHECK(prefractal_test_finish(&s, 1, sig) == 1);
}
/* partial_sig_verify is the identifiable-abort tool, so it has to say no to
* everything that is not exactly the share it was asked about. */
static void run_prefractal_partial_sig_verify_test(void) {
prefractal_test_session s;
secp256k1_frost_partial_sig psig, altered;
uint32_t wrong_ids[SECP256K1_FROST_MAX_PARTICIPANTS];
unsigned char other_msg[32];
unsigned int k;
prefractal_test_setup(&s, 5, 3, prefractal_test_seckey_odd, 1, NULL);
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 1);
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0],
s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg) == 1);
/* A tampered share. */
altered = psig;
altered.data[10] ^= 0x40;
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &altered, &s.pubnonces[0], &s.pubshares[0],
s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg) == 0);
/* The right share against the wrong member's public share and nonce. */
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[1], &s.pubshares[1],
s.ids[1], s.ids, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg) == 0);
/* A different signer set. lambda_i is defined over the participating set,
* so a share made for one set does not verify under another even though
* every other argument is unchanged. */
for (k = 0; k < s.t; k++) {
wrong_ids[k] = s.ids[k];
}
wrong_ids[s.t - 1] = s.ids[s.t];
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0],
s.ids[0], wrong_ids, s.t, &s.aggnonce,
&s.thresh_pk, &s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg) == 0);
/* A different message gives the same 0 as a bad share, which is why a 0 is
* not by itself evidence about a member. */
memcpy(other_msg, s.msg, 32);
other_msg[0] ^= 1;
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0],
s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, other_msg) == 0);
}
/* A non-identity frost tweak cache is refused by all three entry points that
* take one. The check is in sign as well as agg so the key a member signs
* under is tied to the cache that was validated. */
static void run_prefractal_identity_cache_test(void) {
prefractal_test_session s;
secp256k1_frost_tweak_cache tweaked;
secp256k1_frost_partial_sig psig;
const secp256k1_frost_partial_sig *psig_ptrs[1];
secp256k1_musig_partial_sig out;
secp256k1_xonly_pubkey ignored;
unsigned char tweak[32];
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL);
testrand256(tweak);
tweaked = s.tweak_cache;
CHECK(secp256k1_frost_pubkey_xonly_tweak_add(CTX, &ignored, &tweaked, tweak) == 1);
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&tweaked, &s.keyagg_cache, &s.cosigner_aggnonce, s.msg) == 0);
/* A good share, then verification and aggregation handed the tweaked
* cache. The secnonce above was consumed even on the failure path, so this
* uses the other member's. */
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[1], s.secshares[1], s.ids[1],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 1);
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[1], &s.pubshares[1],
s.ids[1], s.ids, s.t, &s.aggnonce, &s.thresh_pk,
&tweaked, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg) == 0);
psig_ptrs[0] = &psig;
CHECK(secp256k1_prefractal_partial_sig_agg(CTX, &out, NULL, psig_ptrs, 1, &tweaked) == 0);
/* The identity cache is accepted at the same call, so the refusal above is
* about the tweak and not about the arguments in general. */
CHECK(secp256k1_prefractal_partial_sig_agg(CTX, &out, NULL, psig_ptrs, 1, &s.tweak_cache) == 1);
}
/* thresh_pk and the tweak cache have to describe the same key. Passing a cache
* built for some other group is the mistake this catches. */
static void run_prefractal_key_cache_mismatch_test(void) {
prefractal_test_session s;
secp256k1_frost_tweak_cache other_cache;
secp256k1_pubkey other_pk, other_pubshares[8];
unsigned char other_secshares[8][32];
secp256k1_frost_partial_sig psig;
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL);
CHECK(secp256k1_frost_trusted_dealer_keygen(CTX, other_secshares[0], &other_pk,
other_pubshares, 3, 2,
prefractal_test_seckey_even) == 1);
CHECK(secp256k1_frost_tweak_cache_init(CTX, &other_cache, &other_pk) == 1);
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&other_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 0);
}
/* A musig pubnonce cannot encode the point at infinity, so a group whose
* aggregate nonce lands there has to be told rather than handed something
* unusable. Either column can do it independently: the first is passed through
* unscaled, the second is scaled by b_frost first.
*
* Both are reached the same way. A pubnonce and its negation sum to infinity
* in both columns at once, so the first column is tested with that pair, and
* for the second the members' second-column points are made to cancel while
* the first column does not. */
static void run_prefractal_infinity_nonce_test(void) {
prefractal_test_session s;
secp256k1_frost_pubnonce negated;
const secp256k1_frost_pubnonce *ptrs[2];
secp256k1_musig_pubnonce pubnonce_out;
secp256k1_frost_aggnonce aggnonce_out;
unsigned char ser[66], neg_ser[66];
secp256k1_pubkey pt;
uint32_t ids[2];
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL);
ids[0] = s.ids[0];
ids[1] = s.ids[1];
/* Negate both columns of member 0's nonce: every column then sums to
* infinity. */
CHECK(secp256k1_frost_pubnonce_serialize(CTX, ser, &s.pubnonces[0]) == 1);
CHECK(secp256k1_ec_pubkey_parse(CTX, &pt, &ser[0], 33) == 1);
CHECK(secp256k1_ec_pubkey_negate(CTX, &pt) == 1);
{
size_t len = 33;
CHECK(secp256k1_ec_pubkey_serialize(CTX, &neg_ser[0], &len, &pt, SECP256K1_EC_COMPRESSED) == 1);
}
CHECK(secp256k1_ec_pubkey_parse(CTX, &pt, &ser[33], 33) == 1);
CHECK(secp256k1_ec_pubkey_negate(CTX, &pt) == 1);
{
size_t len = 33;
CHECK(secp256k1_ec_pubkey_serialize(CTX, &neg_ser[33], &len, &pt, SECP256K1_EC_COMPRESSED) == 1);
}
CHECK(secp256k1_frost_pubnonce_parse(CTX, &negated, neg_ser) == 1);
ptrs[0] = &s.pubnonces[0];
ptrs[1] = &negated;
CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, ptrs, ids, 2,
&s.thresh_pk) == 0);
/* Now cancel only the second column: first column R1 + R1 is not infinity,
* second column R2 + (-R2) is. This is the scaled component, so it reaches
* the guard only after the b_frost multiplication. */
memcpy(&neg_ser[0], &ser[0], 33);
CHECK(secp256k1_frost_pubnonce_parse(CTX, &negated, neg_ser) == 1);
CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, ptrs, ids, 2,
&s.thresh_pk) == 0);
/* The untouched set still works, so the refusals above are about the
* infinities and not about this arrangement of arguments. */
ptrs[1] = &s.pubnonces[1];
CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, ptrs, ids, 2,
&s.thresh_pk) == 1);
}
/* A secnonce is single use. sign wipes it, so the second call cannot produce
* the second signature that would expose the share. The wiped secnonce is
* caught by the magic check inside secp256k1_frost_secnonce_load, which is an
* ARG_CHECK and so reaches the illegal-argument callback rather than returning
* 0 - the same way stock secp256k1_frost_sign behaves. */
static void run_prefractal_nonce_reuse_test(void) {
prefractal_test_session s;
secp256k1_frost_partial_sig psig;
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL);
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 1);
CHECK(secp256k1_is_zero_array(s.secnonces[0].data, sizeof(s.secnonces[0].data)));
CHECK_ILLEGAL(CTX, secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0],
s.ids[0], s.ids, s.pubshares, s.t, &s.aggnonce,
&s.thresh_pk, &s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg));
/* Every call that reaches the secnonce load wipes it, including the ones
* that then fail for another reason. A member whose signing attempt was
* refused must not retry with the same nonce. */
CHECK(secp256k1_is_zero_array(s.secnonces[1].data, sizeof(s.secnonces[1].data)) == 0);
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[1], s.secshares[1], s.ids[2],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 0);
CHECK(secp256k1_is_zero_array(s.secnonces[1].data, sizeof(s.secnonces[1].data)));
}
/* Negative controls: shares that are individually well formed and only fail at
* the very end, against the outer aggregate. These are the failures the module
* cannot catch for the caller, so the tests pin where they do surface. */
static void run_prefractal_negative_control_test(void) {
prefractal_test_session s;
unsigned char sig[64], tweak[32];
/* Wrong key order: the group signs under a cache that aggregates the two
* keys in the other order, so a_musig is wrong. Every partial signature is
* valid on its own terms and the final signature does not verify. */
testrand256(tweak);
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL);
{
const secp256k1_pubkey *swapped[2];
secp256k1_musig_keyagg_cache wrong_cache;
secp256k1_xonly_pubkey ignored;
swapped[0] = &s.cosigner_pk;
swapped[1] = &s.thresh_pk;
CHECK(secp256k1_musig_pubkey_agg(CTX, &ignored, &wrong_cache, swapped, 2) == 1);
s.keyagg_cache = wrong_cache;
/* finish() would trip its own internal partial_sig_verify CHECKs only
* if the shares disagreed with the session; they do not, because the
* session is rebuilt from this same wrong cache. What fails is the
* final BIP 340 verification against the ORIGINAL aggregate key. */
CHECK(prefractal_test_finish(&s, 1, sig) == 0);
}
/* Missing tweak: the group signs under the untweaked outer cache while the
* signature is checked against the tweaked aggregate key. */
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, tweak);
{
const secp256k1_pubkey *pubkeys[2];
secp256k1_musig_keyagg_cache untweaked;
secp256k1_xonly_pubkey ignored;
pubkeys[0] = &s.thresh_pk;
pubkeys[1] = &s.cosigner_pk;
CHECK(secp256k1_musig_pubkey_agg(CTX, &ignored, &untweaked, pubkeys, 2) == 1);
s.keyagg_cache = untweaked;
CHECK(prefractal_test_finish(&s, 1, sig) == 0);
}
}
/* Argument checking, and the bounds the module states in its header. */
static void run_prefractal_api_test(void) {
prefractal_test_session s;
secp256k1_musig_pubnonce pubnonce_out;
secp256k1_frost_aggnonce aggnonce_out;
secp256k1_frost_partial_sig psig;
const secp256k1_frost_partial_sig *psig_ptrs[1];
secp256k1_musig_partial_sig out;
uint32_t dup_ids[2];
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_even, 1, NULL);
/* Duplicate identifiers make lambda_i undefined, so nonce_agg refuses
* rather than producing a nonce nobody can sign against. */
dup_ids[0] = s.ids[0];
dup_ids[1] = s.ids[0];
CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, s.pubnonce_ptrs,
dup_ids, 2, &s.thresh_pk) == 0);
/* n_signers out of range is a caller bug, not a peer-influenced value, so
* it takes the illegal-argument path rather than returning 0 - the same
* choice the frost module makes for its own counts. */
CHECK_ILLEGAL(CTX, secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out,
s.pubnonce_ptrs, s.ids, 0, &s.thresh_pk));
/* An id that is not in the signer set. */
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[2],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 0);
/* A secret share that does not match the pubshare it is claimed for. */
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[1], s.secshares[0], s.ids[1],
s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 0);
/* Passing NULL pubshares skips that check, which is what the header says it
* does; the signature is still correct. */
prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_even, 1, NULL);
CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0],
s.ids, NULL, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce,
s.msg) == 1);
CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0],
s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk,
&s.tweak_cache, &s.keyagg_cache,
&s.cosigner_aggnonce, s.msg) == 1);
/* n_sigs out of range, likewise. */
psig_ptrs[0] = &psig;
CHECK_ILLEGAL(CTX, secp256k1_prefractal_partial_sig_agg(CTX, &out, NULL, psig_ptrs, 0,
&s.tweak_cache));
}
/* The tagged hash this module defines. Pinning the midstate here is what keeps
* a rewrite of the constant honest: nothing else in the tree would notice a
* changed b_frost, it would just produce signatures that do not verify. */
static void run_prefractal_midstate_test(void) {
secp256k1_sha256 sha, sha_tagged;
unsigned char tag[] = "Prefractal/noncecoef";
unsigned char buf[32], buf_tagged[32];
secp256k1_sha256_initialize_tagged(secp256k1_get_hash_context(CTX), &sha, tag, sizeof(tag) - 1);
secp256k1_prefractal_noncecoef_sha256_tagged(&sha_tagged);
secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &sha, buf);
secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &sha_tagged, buf_tagged);
CHECK(secp256k1_memcmp_var(buf, buf_tagged, 32) == 0);
}
static const struct tf_test_entry tests_prefractal[] = {
CASE1(run_prefractal_midstate_test),
CASE1(run_prefractal_e2e_test),
CASE1(run_prefractal_odd_y_group_key_test),
CASE1(run_prefractal_partial_sig_verify_test),
CASE1(run_prefractal_identity_cache_test),
CASE1(run_prefractal_key_cache_mismatch_test),
CASE1(run_prefractal_infinity_nonce_test),
CASE1(run_prefractal_nonce_reuse_test),
CASE1(run_prefractal_negative_control_test),
CASE1(run_prefractal_api_test),
};
#endif /* SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H */

View File

@@ -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

View File

@@ -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