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.
236 lines
12 KiB
C
236 lines
12 KiB
C
#ifndef SECP256K1_PREFRACTAL_H
|
|
#define SECP256K1_PREFRACTAL_H
|
|
|
|
#include "secp256k1_frost.h"
|
|
#include "secp256k1_musig.h"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
/** This module implements a nested FROST+MuSig2 signing scheme ("prefractal"),
|
|
* which lets a FROST t-of-n group occupy ONE participant slot of an ordinary
|
|
* MuSig2 (BIP 327) session.
|
|
*
|
|
* WARNING: EXPERIMENTAL. Neither the scheme nor this implementation has been
|
|
* reviewed by anyone outside the project, and should not be used to protect
|
|
* anything of value.
|
|
*
|
|
* The construction is the nested signing scheme of frosty-musig
|
|
* (https://github.com/jesseposner/frosty-musig). Each group member computes
|
|
*
|
|
* s_i = k1_i + b_frost*b_musig*k2_i + e*a*lambda_i*g*gacc*d_i
|
|
*
|
|
* where b_frost is this module's nonce-binding coefficient and b_musig, e, a,
|
|
* g and gacc all belong to the OUTER MuSig2 session. The group publishes one
|
|
* ordinary MuSig2 public nonce and one ordinary MuSig2 partial signature, so
|
|
* cosigners cannot tell a group is involved and need no support for it.
|
|
*
|
|
* DELIBERATE DEVIATIONS FROM BIP 445 (see doc/prefractal.md):
|
|
*
|
|
* 1. b_frost does NOT commit to the message. BIP 445's nonce coefficient
|
|
* hashes the message, but this module is built for protocols that publish
|
|
* nonces before the message exists. The OUTER coefficient b_musig does
|
|
* commit to the message and binds b_frost through it, exactly as the
|
|
* iceberg module does with its own Iceberg/noncecoef tag.
|
|
*
|
|
* 2. There is NO g_frost factor. Stock FROST normalizes its threshold public
|
|
* key to even Y (see g_times_gacc_parity in the frost module), because it
|
|
* produces a BIP 340 x-only signature. Here the threshold public key is an
|
|
* inner participant of the outer key aggregation and is used as a FULL
|
|
* point, so all key-side parity normalization happens once, at the
|
|
* aggregate level, using the OUTER keyagg cache. Applying the frost-level
|
|
* factor would break the relation for every group whose key has odd Y.
|
|
*
|
|
* 3. The FROST tweak cache must be the identity (tacc == 0, gacc_parity == 0).
|
|
* The channel protocols this module targets tweak only the outer aggregate
|
|
* key. Every entry point below checks this rather than silently ignoring a
|
|
* tweaked cache.
|
|
*
|
|
* NONCE HANDLING: the caller supplies secnonces produced by
|
|
* secp256k1_frost_nonce_gen. As always, a secnonce MUST be used for exactly
|
|
* one signature; reuse across two different messages leaks the secret share
|
|
* and nothing here can detect it.
|
|
*
|
|
* The round-two signer set must be EXACTLY the round-one set: the Lagrange
|
|
* coefficients and the aggregate nonce are both defined over the
|
|
* participating set, so a proper subset produces an invalid signature with no
|
|
* error raised. Pass the same ids array to every function below.
|
|
*/
|
|
|
|
/** Aggregate the group members' public nonces and export the group's
|
|
* OUTER-wire nonce.
|
|
*
|
|
* pubnonce_out is an ordinary MuSig2 public nonce, (R1, b_frost*R2), which is
|
|
* what the group sends to its cosigners. aggnonce_out is the UNSCALED FROST
|
|
* aggregate nonce, which the members need later for partial signing; it is an
|
|
* internal value and must be given back to secp256k1_prefractal_sign and
|
|
* secp256k1_prefractal_partial_sig_verify unchanged.
|
|
*
|
|
* b_frost = tagged_hash("Prefractal/noncecoef",
|
|
* ser32(u) || sorted ser32 ids || aggnonce66 ||
|
|
* cbytes_ext(thresh_pk))
|
|
*
|
|
* Returns: 1 on success, 0 if a pubnonce could not be loaded, if the ids are
|
|
* invalid, or if either output nonce component is the point at
|
|
* infinity. A FROST aggregate nonce component may legitimately be
|
|
* infinity (BIP 445 NonceAgg), but a MuSig2 public nonce has no
|
|
* encoding for it, so such a session must be restarted with fresh
|
|
* nonces.
|
|
* Args: ctx: pointer to a context object
|
|
* Out: pubnonce_out: the group's MuSig2 public nonce
|
|
* aggnonce_out: the group's unscaled FROST aggregate nonce
|
|
* In: pubnonces: array of pointers to the members' public nonces
|
|
* ids: array of the members' identifiers
|
|
* n_signers: number of members (must match the array lengths, at
|
|
* least 1 and at most SECP256K1_FROST_MAX_PARTICIPANTS)
|
|
* thresh_pk: the group's (untweaked) threshold public key
|
|
*/
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_nonce_agg(
|
|
const secp256k1_context *ctx,
|
|
secp256k1_musig_pubnonce *pubnonce_out,
|
|
secp256k1_frost_aggnonce *aggnonce_out,
|
|
const secp256k1_frost_pubnonce *const *pubnonces,
|
|
const uint32_t *ids,
|
|
size_t n_signers,
|
|
const secp256k1_pubkey *thresh_pk
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(7);
|
|
|
|
/** Produce one group member's nested partial signature.
|
|
*
|
|
* Computes s_i = k1 + b_frost*b_musig*k2 + e*a*lambda_i*g*gacc*d_i, with both
|
|
* nonce scalars negated iff the OUTER final nonce has odd Y, and the key-side
|
|
* factor g*gacc taken from the OUTER keyagg cache. There is deliberately no
|
|
* g_frost factor (see the module notes above).
|
|
*
|
|
* The secnonce is wiped, so a second call with the same secnonce fails. The
|
|
* partial signature is self-verified before it is returned, as BIP 445
|
|
* recommends.
|
|
*
|
|
* Returns: 1 on success, 0 on failure. Failure cases include: an invalidated
|
|
* or malformed secnonce, an invalid secret share, my_id not in ids,
|
|
* a secret share that does not match its pubshare, a non-identity
|
|
* tweak_cache, a nonce component at infinity, and a failed
|
|
* self-verification.
|
|
* Args: ctx: pointer to a context object
|
|
* Out: partial_sig: the member's partial signature
|
|
* In/Out: secnonce: the member's secret nonce, wiped by this call
|
|
* In: secshare32: the member's 32-byte secret share
|
|
* my_id: the member's identifier
|
|
* ids: array of the participating members' identifiers,
|
|
* the SAME array given to _nonce_agg
|
|
* pubshares: array of the members' public shares, in the order
|
|
* of ids, or NULL to skip the share/pubshare check
|
|
* (providing them is recommended)
|
|
* n_signers: number of members
|
|
* aggnonce: the unscaled FROST aggregate nonce from _nonce_agg
|
|
* thresh_pk: the group's (untweaked) threshold public key
|
|
* tweak_cache: the group's FROST tweak cache, which MUST be the
|
|
* identity
|
|
* keyagg_cache: the OUTER MuSig2 keyagg cache, already carrying any
|
|
* BIP 341 tweak
|
|
* cosigner_aggnonce: the aggregate of the NON-group participants'
|
|
* MuSig2 public nonces
|
|
* msg32: the 32-byte message being signed
|
|
*/
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_sign(
|
|
const secp256k1_context *ctx,
|
|
secp256k1_frost_partial_sig *partial_sig,
|
|
secp256k1_frost_secnonce *secnonce,
|
|
const unsigned char *secshare32,
|
|
uint32_t my_id,
|
|
const uint32_t *ids,
|
|
const secp256k1_pubkey *pubshares,
|
|
size_t n_signers,
|
|
const secp256k1_frost_aggnonce *aggnonce,
|
|
const secp256k1_pubkey *thresh_pk,
|
|
const secp256k1_frost_tweak_cache *tweak_cache,
|
|
const secp256k1_musig_keyagg_cache *keyagg_cache,
|
|
const secp256k1_musig_aggnonce *cosigner_aggnonce,
|
|
const unsigned char *msg32
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11) SECP256K1_ARG_NONNULL(12) SECP256K1_ARG_NONNULL(13) SECP256K1_ARG_NONNULL(14);
|
|
|
|
/** Verify one group member's nested partial signature.
|
|
*
|
|
* Checks s_i*G == R1_i + b_frost*b_musig*R2_i + e*a*lambda_i*g*gacc*P_i, with
|
|
* the nonce points negated iff the OUTER final nonce has odd Y. The session is
|
|
* recomputed from the same parameters secp256k1_prefractal_sign takes, so the
|
|
* caller must pass exactly the same ids, aggnonce, keys and message.
|
|
*
|
|
* This is the identifiable-abort tool: when the aggregate signature fails,
|
|
* running this over each member's share names the one at fault.
|
|
*
|
|
* Returns: 1 if the partial signature is valid, 0 otherwise.
|
|
* Args: ctx: pointer to a context object
|
|
* In: partial_sig: the partial signature to verify
|
|
* pubnonce: the member's public nonce, as given to _nonce_agg
|
|
* pubshare: the member's public share
|
|
* my_id: the member's identifier
|
|
* ids: array of the participating members' identifiers
|
|
* n_signers: number of members
|
|
* aggnonce: the unscaled FROST aggregate nonce from _nonce_agg
|
|
* thresh_pk: the group's (untweaked) threshold public key
|
|
* tweak_cache: the group's FROST tweak cache, which MUST be the
|
|
* identity
|
|
* keyagg_cache: the OUTER MuSig2 keyagg cache
|
|
* cosigner_aggnonce: the aggregate of the NON-group participants' nonces
|
|
* msg32: the 32-byte message being signed
|
|
*/
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_partial_sig_verify(
|
|
const secp256k1_context *ctx,
|
|
const secp256k1_frost_partial_sig *partial_sig,
|
|
const secp256k1_frost_pubnonce *pubnonce,
|
|
const secp256k1_pubkey *pubshare,
|
|
uint32_t my_id,
|
|
const uint32_t *ids,
|
|
size_t n_signers,
|
|
const secp256k1_frost_aggnonce *aggnonce,
|
|
const secp256k1_pubkey *thresh_pk,
|
|
const secp256k1_frost_tweak_cache *tweak_cache,
|
|
const secp256k1_musig_keyagg_cache *keyagg_cache,
|
|
const secp256k1_musig_aggnonce *cosigner_aggnonce,
|
|
const unsigned char *msg32
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11) SECP256K1_ARG_NONNULL(12) SECP256K1_ARG_NONNULL(13);
|
|
|
|
/** Sum the members' partial signatures into one ordinary MuSig2 partial
|
|
* signature.
|
|
*
|
|
* The result is ready for secp256k1_musig_partial_sig_agg alongside the
|
|
* cosigners' partial signatures. The sum is plain because the FROST tweak
|
|
* cache is required to be the identity: with a tweak there would be an
|
|
* additional e*g*tacc term to fold in, and this module does not support that.
|
|
*
|
|
* This does not check that the shares are the ones the members would have
|
|
* produced; secp256k1_prefractal_partial_sig_verify answers that, one share at
|
|
* a time.
|
|
*
|
|
* Returns: 1 on success, 0 if a partial signature could not be loaded or if
|
|
* tweak_cache is not the identity.
|
|
* Args: ctx: pointer to a context object
|
|
* Out: sig_out: the resulting MuSig2 partial signature
|
|
* error_index: if non-NULL and a partial signature fails to load,
|
|
* receives its index in partial_sigs
|
|
* In: partial_sigs: array of pointers to the members' partial signatures
|
|
* n_sigs: number of partial signatures (at least 1 and at most
|
|
* SECP256K1_FROST_MAX_PARTICIPANTS)
|
|
* tweak_cache: the group's FROST tweak cache, which MUST be the
|
|
* identity
|
|
*/
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_prefractal_partial_sig_agg(
|
|
const secp256k1_context *ctx,
|
|
secp256k1_musig_partial_sig *sig_out,
|
|
size_t *error_index,
|
|
const secp256k1_frost_partial_sig *const *partial_sigs,
|
|
size_t n_sigs,
|
|
const secp256k1_frost_tweak_cache *tweak_cache
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* SECP256K1_PREFRACTAL_H */
|