#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 */