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