prefractal: add the nested FROST+MuSig2 module (API, implementation, wiring)

Adds `prefractal`, an experimental module that lets a FROST t-of-n group
occupy ONE participant slot of an ordinary MuSig2 (BIP 327) session. Each
member computes

    s_i = k1_i + b_frost*b_musig*k2_i + e*a*lambda_i*g*gacc*d_i

and the group publishes one ordinary MuSig2 public nonce and one ordinary
MuSig2 partial signature, so cosigners need no support for it and cannot tell
a group is involved.

Four public functions, all sessionless (every call takes its session
parameters explicitly, so there are no new opaque types, magics or *_SIZE
constants to keep synchronised):

  secp256k1_prefractal_nonce_agg           group wire nonce + unscaled aggnonce
  secp256k1_prefractal_sign                one member's partial signature
  secp256k1_prefractal_partial_sig_verify  identifiable abort
  secp256k1_prefractal_partial_sig_agg     sum -> musig partial signature

Three deliberate deviations from BIP 445, all documented in the public header:

1. b_frost does not commit to the message. The target protocols publish the
   group's wire nonce before the message exists, so a message-committing
   coefficient could not be computed in round one and rebuilt later. The outer
   b_musig does commit to the message and multiplies this one, so the product
   still binds it. Same trade the iceberg module makes, for the same reason.
   The preimage is BIP 445's with the message dropped and the group key
   carried in full rather than x-only, since it is used as a full point
   downstream.

2. There is NO g_frost factor. Stock FROST normalises its threshold key to
   even Y (g_times_gacc_parity = gacc_parity ^ pk_odd, frost/session_impl.h
   :664) because it produces a BIP 340 x-only signature. Here the threshold
   key is an inner participant of the outer key aggregation and is used as a
   full point, so all key-side parity normalisation happens once, at the
   aggregate level, off the OUTER keyagg cache. Note this is NOT implied by
   the tweak cache being the identity: with an identity cache g_frost is still
   -1 for every odd-Y group key, i.e. about half of them. Importing frost's
   key-side parity here would yield a signer that works for even-Y groups and
   fails for odd-Y ones.

3. The FROST tweak cache must be the identity (tacc == 0, gacc_parity == 0).
   Checked in sign and partial_sig_verify, not only in partial_sig_agg, so the
   key a member signs under is tied to the cache that was validated; sign and
   verify additionally require thresh_pk to equal the cache's own key so the
   two arguments cannot disagree.

The verification equation lives in one helper used both by sign's BIP 445
self-check and by partial_sig_verify, so the two cannot drift apart.

Build wiring. Three files order their module blocks differently and the
constraints point in opposite directions:

  - src/secp256k1.c: the include goes AFTER frost and musig, because the
    module calls their static internals.
  - src/CMakeLists.txt: the block goes BEFORE both, because its set() calls
    are only observed by blocks that run later.
  - configure.ac: the block likewise goes before the musig block, NOT at
    iceberg's position further down. configure.ac orders musig and frost ahead
    of iceberg, and iceberg's late enable_module_musig=yes is harmless only
    because musig defaults to yes. frost defaults to no, so a late
    force-enable would leave -DENABLE_MODULE_FROST=1 unemitted while
    AM_CONDITIONAL still observed the mutation - a library whose secp256k1.c
    never included frost, built alongside frost's own sources.

frost is also the first default-OFF module anything depends on, which breaks
the dependency-guard idiom used everywhere else in both build systems: the
existing "DEFINED X AND NOT X" (CMake) and "x$X = xno" (autotools) tests read
as "the user disabled it explicitly" only for default-ON modules, and are true
by default for a default-OFF one. Since neither build system can distinguish
an explicit disable from the default once both are in the cache, enabling
prefractal simply implies frost; the guard is kept for musig, where it still
means what it says. The CMake block additionally lifts both dependencies into
the parent scope so the top-level configuration summary reports what was
actually built rather than printing "frost OFF" while compiling frost in.

Verified on both build systems:

  cmake -B build -DSECP256K1_ENABLE_MODULE_PREFRACTAL=ON -DSECP256K1_BUILD_TESTS=ON
      -> musig/frost/prefractal all ON, tests pass, 4 prefractal symbols exported
  cmake -B build -DSECP256K1_BUILD_TESTS=ON
      -> prefractal OFF, default build unchanged, tests pass
  ./configure --enable-experimental --enable-module-prefractal && make && make check
      -> frost=yes forced on, -DENABLE_MODULE_FROST=1 emitted, 3/3 pass
  ./configure --enable-module-prefractal
      -> correctly refused: "Prefractal module is experimental"

tests_impl.h is a placeholder here so the module links; the real suite lands
next.
This commit is contained in:
Kgothatso Ngako
2026-09-04 00:44:43 +02:00
parent 03db82815f
commit 903da53c06
11 changed files with 898 additions and 0 deletions

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,14 @@
#ifndef SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H
#define SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H
#include "../../../include/secp256k1_prefractal.h"
static void run_prefractal_smoke_test(void) {
CHECK(1);
}
static const struct tf_test_entry tests_prefractal[] = {
CASE1(run_prefractal_smoke_test),
};
#endif /* SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H */