build: wire the frost_enrollment module into both build systems
Second of six commits adding the frost_enrollment module. This one is
scaffolding only: the five entry points are stubs that validate their
pointer arguments, zero their outputs and return 0. What is being
verified here is that the module configures, compiles, links, exports
its symbols and registers its test module in both build systems -- so
that the next commit changes nothing but arithmetic.
Ordering is the one thing in this commit that can go silently wrong, and
it goes wrong in opposite directions in the two build systems:
- configure.ac executes its `if` blocks in file order, and
enable_module_frost defaults to no (configure.ac:243). A block placed
after the frost block at :601 that sets enable_module_frost=yes flips
the variable too late: AM_CONDITIONAL goes true, so the header is
installed and the Makefile fragment is pulled in, but
-DENABLE_MODULE_FROST=1 is never appended, so src/secp256k1.c never
includes frost's implementation and every secp256k1_frost_* symbol
fails to link. The new block therefore goes ahead of both the frost
block and prefractal's, which documents the same trap.
- src/CMakeLists.txt processes dependents FIRST, so the same block goes
above the FROST block there, beside prefractal's.
Verified rather than assumed: configuring with ONLY
--enable-module-frost-enrollment emits -DENABLE_MODULE_FROST=1
alongside -DENABLE_MODULE_FROST_ENROLLMENT=1, and the CMake summary
prints "frost ON" for the same configuration -- the latter is what the
PARENT_SCOPE lift buys, since the summary runs after
add_subdirectory(src) and would otherwise report a module it is
compiling in as OFF.
The dependency guard is prefractal's implies-frost idiom, copied
verbatim along with its reasoning. frost is default-OFF, so the
`test x"$enable_module_frost" = x"no"` / `DEFINED X AND NOT X` guard
every other module uses -- which reads as "the user disabled it
explicitly" for a default-ON dependency -- is true by default here and
cannot tell an explicit --disable-module-frost from the default once
both are in the cache. Enabling frost-enrollment simply implies frost,
with no error.
The one frost-module change in the whole series is in this commit:
src/modules/frost/session.h gains a declaration for
secp256k1_frost_sort_ids, which is defined at session_impl.h:517 and
declared nowhere. The params hash needs it to canonicalize identifier
order. Prefractal reaches frost's statics through translation-unit
ordering alone; rather than inherit reuse-by-link-order, this declares
the function where keygen.h:48 already declares derive_pubshare_at, so
the reuse goes through an interface. No behavior change: it is a
declaration for an existing static definition in the same TU.
CI wiring is two files, and skipping either half fails quietly:
- ci/ci.sh gets FROST_ENROLLMENT in the reproduction header's variable
list and --enable-module-frost-enrollment="$FROST_ENROLLMENT" after
the prefractal line.
- .github/workflows/ci.yml gets FROST_ENROLLMENT at every PREFRACTAL
site: the global default, 11 inline matrix entries and 10 job-level
env blocks. Without the default, ci.sh runs under set -eux with an
empty $FROST_ENROLLMENT, passes --enable-module-frost-enrollment="",
`test x"" = x"yes"` is false, and the module is off in all of CI while
ci.sh visibly has the plumbing.
Verified programmatically over the parsed workflow: across the 106
effective job contexts, PREFRACTAL and FROST_ENROLLMENT now agree in
every single one (45 set to yes, no mismatches), no context sets
FROST_ENROLLMENT without FROST or without EXPERIMENTAL, and no context
leaves it undefined. ci.sh passes sh -n.
The stub test is not a placeholder that has to be deleted later: every
entry point must reject an empty helper set and leave its output zeroed,
which is true of the stubs and stays true of the finished
implementation, so it doubles as the check that all five symbols are
reachable from the test binary.
Verification. Autotools: ./autogen.sh, then a frost-enrollment-only
configure and a full configure with frost, chilldkg, iceberg, prefractal
and frost-enrollment all on -- both build with zero warnings under the
project's -Werror-grade flag set, ./tests and ./exhaustive_tests exit 0,
and `./tests -l` lists the frost_enrollment module. CMake: configure with
-DSECP256K1_EXPERIMENTAL=ON -DSECP256K1_ENABLE_MODULE_FROST_ENROLLMENT=ON
builds clean and ctest passes 391 tests. nm shows the five new symbols
exported from libsecp256k1.so; tools/symbol-check.py could not be run
here because python3-lief is not installed in this environment, but all
five carry the required secp256k1_ prefix. make dist succeeds and the
tarball carries src/modules/frost_enrollment/frost_enrollment.md
alongside the other module documents.
One unrelated observation from this build: a stale
src/ctime_tests-ctime_tests.o left over from an earlier configure with a
different module set will fail to link, because automake does not track
CPPFLAGS changes across reconfigures. make clean between configurations
with different module sets, not a fault in this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 03:53:03 +02:00
|
|
|
/***********************************************************************
|
|
|
|
|
* Distributed under the MIT software license, see the accompanying *
|
|
|
|
|
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
|
|
|
|
|
***********************************************************************/
|
|
|
|
|
|
|
|
|
|
#ifndef SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H
|
|
|
|
|
#define SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H
|
|
|
|
|
|
|
|
|
|
#include "../../../include/secp256k1_frost_enrollment.h"
|
|
|
|
|
|
frost_enrollment: implement the three rounds
Third of six commits. Replaces the Phase 1 stubs with the real
arithmetic, adds a smoke test that a 2-of-3 group really does grow into
a working 2-of-4 one, and wires the entry points into ctime_tests.
The Lagrange machinery is frost's, called in place. pubshare_derive is a
skin over secp256k1_frost_derive_pubshare_at
(src/modules/frost/keygen_impl.h:150) evaluated at identifier new_id,
and the id canonicalization is secp256k1_frost_sort_ids, reached through
the declaration the previous commit added.
The one piece frost could not supply is the scalar Lagrange coefficient
at an arbitrary point. frost's secp256k1_frost_derive_interpolating_value
evaluates at x-coordinate 0, which is what reconstructing the group
secret needs; enrollment needs the basis polynomial at the TARGET
x-coordinate. secp256k1_frost_enrollment_lagrange_at is that, and it is
deliberately the same product derive_pubshare_at applies to each
pubshare, in the same identifier space -- so the scalar path and the
point path agree by construction rather than by coincidence. Working in
identifier space is what makes the id-to-x-coordinate +1 cancel: an
x-coordinate difference x_j - x_i is the identifier difference
id_j - id_i.
Round 1.1 computes v = lambda * secshare and splits it. Every share but
the one kept locally is masking randomness derived as
Scalar.from_bytes_wrapping(
TH("FROST enrollment/share_split",
rand32 || params_hash32 || ser32(my_id) || ser32(recipient_id)))
with rand32 = TH(same tag, session_secrand32) XOR secshare32; the kept
share absorbs the remainder so the set sums to v. Three details:
- The reduction wraps rather than rejects, chilldkg's
from_bytes_wrapping (src/modules/chilldkg/util_impl.h:394). A 256-bit
hash mod the group order is about 2^-128 from uniform; rejection
sampling would buy that back in exchange for a variable-time loop.
- Masking with the secret share is the secp256k1_frost_nonce_gen pattern
(session_impl.h:340), so a broken RNG alone does not reveal the split.
- The derivation is indexed by the recipient's IDENTIFIER, not by its
position in the caller's ids array. The plan called for a counter;
identifiers are unique, so they are one, and using them makes the
split independent of the order a caller lists the helper set in. What
the binding buys is DOMAIN SEPARATION only: params_hash32 carries the
group key and the whole parameter tuple, so two runs sharing a seed
but differing in either cannot produce the same deltas. It cannot
detect a disagreement between helpers, because nothing cross-checks
per-helper private randomness. That is the params hash's job.
session_secrand32 is wiped whether the call succeeds or fails, so a
caller cannot retry a failed run on the same randomness.
Round 1.2 recomputes its own params hash from the group key and the
tuple, compares every received hash against it, then sums. The slot at
the caller's own position in received_params_hashes32 is skipped, while
the same position in all_shares32 is read -- the asymmetry the header
documents, and the thing that makes this a recomputation rather than a
string comparison. The mode and bounds are re-validated here rather than
trusted from the round 1.1 call site, since the full tuple is present.
An out-of-range share is reported through mismatch_id the way
secp256k1_frost_partial_sig_agg reports an unparseable partial
signature.
Round 2 compares the params hash against its own recomputation over the
authenticated group key, sums, rejects a zero share, and checks
secshare*G against the expected public share.
Three deviations from the plan, all to match what the tree already does:
- Value ranges return 0; only pointers get ARG_CHECK. The plan called
for an ARG_CHECK on the n_ids bound, but the frost module's split is
the one used here (secp256k1_frost_trusted_dealer_keygen,
keygen_impl.h:227), and the header already documents these as
return-0 conditions. The bound is still enforced in production builds
-- params_are_valid requires 2 <= threshold <= n_ids <= n_participants
<= 128 -- so it does not ride on the VERIFY_CHECK inside
secp256k1_frost_sort_ids, which is what the plan was guarding against.
- The public-share check declassifies the derived point and compares
with secp256k1_ge_eq_var, rather than comparing 33 serialized bytes in
constant time. There is no constant-time memcmp in this tree, and
secshare*G is a public key: secp256k1_frost_sign declassifies exactly
this quantity before exactly this comparison
(src/modules/frost/session_impl.h:770, :789). Inventing a primitive to
avoid following that precedent would be the worse trade.
- params_hash's public entry point delegates to the same internal
routine every gate uses, so the encoding has exactly one
implementation to keep in step with the vectors.
One real bug found by the tooling rather than by reading. Accumulators
were initialized with secp256k1_scalar_clear, and
secp256k1_memclear_explicit marks its target UNDEFINED in VERIFY builds
(src/util.h:295) precisely so that reading cleared memory is caught. It
was: valgrind reported 143752 errors in share_agg's summation loop.
Accumulators now start at secp256k1_scalar_set_int(x, 0); scalar_clear
is used only where it means "done with this secret". Worth stating
plainly because the failure mode is invisible in a production build,
where memclear_explicit only zeroes.
ctime_tests gains a 2-of-3-enrolls-a-fourth block covering all three
rounds, following prefractal's b66c757b. The threshold key, the secret
shares, the session randomness and every delta and sigma on the wire are
marked secret; the identifiers, public shares, group key, parameters
hashes and derived public share are not. Under valgrind: 0 errors from 0
contexts, so no branch or memory access in the new code depends on
secret data.
Verification: ./tests, ./noverify_tests and ./exhaustive_tests exit 0;
the frost_enrollment module runs clean under valgrind (0 errors); a
separate CPPFLAGS='-DVERIFY' build compiles without warnings and passes;
the module builds warning-free alongside frost, chilldkg, iceberg and
prefractal.
The smoke test is the substantive check: after a 2-of-3 group enrolls
participant 3, every pair {i, 3} for i in 0..2 reconstructs the original
threshold secret and matches the threshold public key, and the untouched
pair {0, 1} still does too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:06:59 +02:00
|
|
|
/* Everything one enrollment run needs, so a test can set one up in a line and
|
|
|
|
|
* then poke at individual pieces. */
|
|
|
|
|
typedef struct {
|
|
|
|
|
size_t n, t, u;
|
|
|
|
|
uint32_t new_id;
|
|
|
|
|
unsigned char thresh_sk[32];
|
|
|
|
|
unsigned char secshares[SECP256K1_FROST_MAX_PARTICIPANTS][32];
|
|
|
|
|
secp256k1_pubkey pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
|
|
|
secp256k1_pubkey thresh_pk;
|
|
|
|
|
uint32_t ids[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
|
|
|
|
|
|
|
|
/* shares[i][j] is what helper ids[i] produced for helper ids[j]. */
|
|
|
|
|
unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
|
|
|
unsigned char params_hashes[SECP256K1_FROST_MAX_PARTICIPANTS][32];
|
|
|
|
|
unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
|
|
|
|
|
|
|
|
secp256k1_pubkey new_pubshare;
|
|
|
|
|
unsigned char new_secshare[32];
|
|
|
|
|
} frost_enrollment_test_run;
|
|
|
|
|
|
|
|
|
|
/* Deals a fresh (t, n) group and fills in the helper set: the first u
|
|
|
|
|
* identifiers that are not new_id, in ascending order. */
|
|
|
|
|
static void frost_enrollment_test_deal(frost_enrollment_test_run *r, size_t n, size_t t, size_t u, uint32_t new_id) {
|
|
|
|
|
size_t i, k;
|
|
|
|
|
|
|
|
|
|
r->n = n;
|
|
|
|
|
r->t = t;
|
|
|
|
|
r->u = u;
|
|
|
|
|
r->new_id = new_id;
|
|
|
|
|
testrand256(r->thresh_sk);
|
|
|
|
|
CHECK(secp256k1_frost_trusted_dealer_keygen(CTX, r->secshares[0], &r->thresh_pk, r->pubshares, n, (uint32_t)t, r->thresh_sk) == 1);
|
|
|
|
|
k = 0;
|
|
|
|
|
for (i = 0; i < n && k < u; i++) {
|
|
|
|
|
if ((uint32_t)i == new_id) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
r->ids[k] = (uint32_t)i;
|
|
|
|
|
k++;
|
|
|
|
|
}
|
|
|
|
|
CHECK(k == u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Runs round 1.1 for every helper. */
|
|
|
|
|
static void frost_enrollment_test_round1_gen(frost_enrollment_test_run *r) {
|
|
|
|
|
size_t i;
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < r->u; i++) {
|
|
|
|
|
unsigned char secrand[32];
|
|
|
|
|
testrand256(secrand);
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r->shares[i], r->params_hashes[i], secrand, r->secshares[r->ids[i]], &r->thresh_pk, r->ids, r->u, r->ids[i], r->new_id, r->n, (uint32_t)r->t) == 1);
|
|
|
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Runs round 1.2 for every helper, transposing the round 1.1 outputs on the
|
|
|
|
|
* way: helper j aggregates entry j of every helper's share buffer. */
|
|
|
|
|
static void frost_enrollment_test_round1_agg(frost_enrollment_test_run *r) {
|
|
|
|
|
size_t i, j;
|
|
|
|
|
|
|
|
|
|
for (j = 0; j < r->u; j++) {
|
|
|
|
|
unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
|
|
|
unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
|
|
|
uint32_t mismatch_id = 0;
|
|
|
|
|
|
|
|
|
|
memset(received, 0, r->u * 32);
|
|
|
|
|
for (i = 0; i < r->u; i++) {
|
|
|
|
|
memcpy(&all_shares[32 * i], &r->shares[i][32 * j], 32);
|
|
|
|
|
if (i != j) {
|
|
|
|
|
memcpy(&received[32 * i], r->params_hashes[i], 32);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r->sigmas[32 * j], &mismatch_id, all_shares, received, &r->thresh_pk, r->ids, r->u, r->ids[j], r->new_id, r->n, (uint32_t)r->t) == 1);
|
|
|
|
|
CHECK(mismatch_id == UINT32_MAX);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Runs round 2, with both optional checks enabled. */
|
|
|
|
|
static void frost_enrollment_test_round2(frost_enrollment_test_run *r) {
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r->new_pubshare, r->pubshares, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t) == 1);
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r->new_secshare, r->sigmas, &r->thresh_pk, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t, r->params_hashes[0], &r->new_pubshare) == 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void frost_enrollment_test_full_run(frost_enrollment_test_run *r, size_t n, size_t t, size_t u, uint32_t new_id) {
|
|
|
|
|
frost_enrollment_test_deal(r, n, t, u, new_id);
|
|
|
|
|
frost_enrollment_test_round1_gen(r);
|
|
|
|
|
frost_enrollment_test_round1_agg(r);
|
|
|
|
|
frost_enrollment_test_round2(r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Reconstructs the threshold secret from the shares of the given identifiers
|
|
|
|
|
* and checks it against the threshold public key. shares[k] must be the share
|
|
|
|
|
* of ids[k]. */
|
|
|
|
|
static void frost_enrollment_test_check_reconstruction(const uint32_t *ids, const unsigned char *const *shares, size_t n_ids, const secp256k1_pubkey *thresh_pk) {
|
|
|
|
|
secp256k1_scalar secret, share, lambda;
|
|
|
|
|
secp256k1_ge pk, expected;
|
|
|
|
|
secp256k1_gej pkj;
|
|
|
|
|
size_t i;
|
|
|
|
|
|
|
|
|
|
secp256k1_scalar_set_int(&secret, 0);
|
|
|
|
|
for (i = 0; i < n_ids; i++) {
|
|
|
|
|
CHECK(secp256k1_frost_derive_interpolating_value(&lambda, ids, n_ids, ids[i]) == 1);
|
|
|
|
|
CHECK(secp256k1_scalar_set_b32_seckey(&share, shares[i]) == 1);
|
|
|
|
|
secp256k1_scalar_mul(&share, &share, &lambda);
|
|
|
|
|
secp256k1_scalar_add(&secret, &secret, &share);
|
|
|
|
|
}
|
|
|
|
|
CHECK(!secp256k1_scalar_is_zero(&secret));
|
|
|
|
|
secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &pkj, &secret);
|
|
|
|
|
secp256k1_ge_set_gej(&pk, &pkj);
|
|
|
|
|
CHECK(secp256k1_pubkey_load(CTX, &expected, thresh_pk) == 1);
|
|
|
|
|
CHECK(secp256k1_ge_eq_var(&pk, &expected) == 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A 2-of-3 group grows to 2-of-4, and the new participant's share sits on the
|
|
|
|
|
* same polynomial as the old ones: every threshold-sized subset that contains
|
|
|
|
|
* it reconstructs the original threshold secret. */
|
|
|
|
|
static void run_frost_enrollment_smoke_test(void) {
|
|
|
|
|
frost_enrollment_test_run r;
|
|
|
|
|
const unsigned char *shares[2];
|
|
|
|
|
uint32_t ids[2];
|
|
|
|
|
size_t i;
|
|
|
|
|
|
|
|
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 3);
|
|
|
|
|
|
|
|
|
|
/* The pair that ran the protocol, and every other pair including the new
|
|
|
|
|
* participant. */
|
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
|
|
|
ids[0] = (uint32_t)i;
|
|
|
|
|
ids[1] = 3;
|
|
|
|
|
shares[0] = r.secshares[i];
|
|
|
|
|
shares[1] = r.new_secshare;
|
|
|
|
|
frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk);
|
|
|
|
|
}
|
|
|
|
|
/* And the old group still reconstructs, unchanged. */
|
|
|
|
|
ids[0] = 0;
|
|
|
|
|
ids[1] = 1;
|
|
|
|
|
shares[0] = r.secshares[0];
|
|
|
|
|
shares[1] = r.secshares[1];
|
|
|
|
|
frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk);
|
|
|
|
|
}
|
|
|
|
|
|
build: wire the frost_enrollment module into both build systems
Second of six commits adding the frost_enrollment module. This one is
scaffolding only: the five entry points are stubs that validate their
pointer arguments, zero their outputs and return 0. What is being
verified here is that the module configures, compiles, links, exports
its symbols and registers its test module in both build systems -- so
that the next commit changes nothing but arithmetic.
Ordering is the one thing in this commit that can go silently wrong, and
it goes wrong in opposite directions in the two build systems:
- configure.ac executes its `if` blocks in file order, and
enable_module_frost defaults to no (configure.ac:243). A block placed
after the frost block at :601 that sets enable_module_frost=yes flips
the variable too late: AM_CONDITIONAL goes true, so the header is
installed and the Makefile fragment is pulled in, but
-DENABLE_MODULE_FROST=1 is never appended, so src/secp256k1.c never
includes frost's implementation and every secp256k1_frost_* symbol
fails to link. The new block therefore goes ahead of both the frost
block and prefractal's, which documents the same trap.
- src/CMakeLists.txt processes dependents FIRST, so the same block goes
above the FROST block there, beside prefractal's.
Verified rather than assumed: configuring with ONLY
--enable-module-frost-enrollment emits -DENABLE_MODULE_FROST=1
alongside -DENABLE_MODULE_FROST_ENROLLMENT=1, and the CMake summary
prints "frost ON" for the same configuration -- the latter is what the
PARENT_SCOPE lift buys, since the summary runs after
add_subdirectory(src) and would otherwise report a module it is
compiling in as OFF.
The dependency guard is prefractal's implies-frost idiom, copied
verbatim along with its reasoning. frost is default-OFF, so the
`test x"$enable_module_frost" = x"no"` / `DEFINED X AND NOT X` guard
every other module uses -- which reads as "the user disabled it
explicitly" for a default-ON dependency -- is true by default here and
cannot tell an explicit --disable-module-frost from the default once
both are in the cache. Enabling frost-enrollment simply implies frost,
with no error.
The one frost-module change in the whole series is in this commit:
src/modules/frost/session.h gains a declaration for
secp256k1_frost_sort_ids, which is defined at session_impl.h:517 and
declared nowhere. The params hash needs it to canonicalize identifier
order. Prefractal reaches frost's statics through translation-unit
ordering alone; rather than inherit reuse-by-link-order, this declares
the function where keygen.h:48 already declares derive_pubshare_at, so
the reuse goes through an interface. No behavior change: it is a
declaration for an existing static definition in the same TU.
CI wiring is two files, and skipping either half fails quietly:
- ci/ci.sh gets FROST_ENROLLMENT in the reproduction header's variable
list and --enable-module-frost-enrollment="$FROST_ENROLLMENT" after
the prefractal line.
- .github/workflows/ci.yml gets FROST_ENROLLMENT at every PREFRACTAL
site: the global default, 11 inline matrix entries and 10 job-level
env blocks. Without the default, ci.sh runs under set -eux with an
empty $FROST_ENROLLMENT, passes --enable-module-frost-enrollment="",
`test x"" = x"yes"` is false, and the module is off in all of CI while
ci.sh visibly has the plumbing.
Verified programmatically over the parsed workflow: across the 106
effective job contexts, PREFRACTAL and FROST_ENROLLMENT now agree in
every single one (45 set to yes, no mismatches), no context sets
FROST_ENROLLMENT without FROST or without EXPERIMENTAL, and no context
leaves it undefined. ci.sh passes sh -n.
The stub test is not a placeholder that has to be deleted later: every
entry point must reject an empty helper set and leave its output zeroed,
which is true of the stubs and stays true of the finished
implementation, so it doubles as the check that all five symbols are
reachable from the test binary.
Verification. Autotools: ./autogen.sh, then a frost-enrollment-only
configure and a full configure with frost, chilldkg, iceberg, prefractal
and frost-enrollment all on -- both build with zero warnings under the
project's -Werror-grade flag set, ./tests and ./exhaustive_tests exit 0,
and `./tests -l` lists the frost_enrollment module. CMake: configure with
-DSECP256K1_EXPERIMENTAL=ON -DSECP256K1_ENABLE_MODULE_FROST_ENROLLMENT=ON
builds clean and ctest passes 391 tests. nm shows the five new symbols
exported from libsecp256k1.so; tools/symbol-check.py could not be run
here because python3-lief is not installed in this environment, but all
five carry the required secp256k1_ prefix. make dist succeeds and the
tarball carries src/modules/frost_enrollment/frost_enrollment.md
alongside the other module documents.
One unrelated observation from this build: a stale
src/ctime_tests-ctime_tests.o left over from an earlier configure with a
different module set will fail to link, because automake does not track
CPPFLAGS changes across reconfigures. make clean between configurations
with different module sets, not a fault in this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 03:53:03 +02:00
|
|
|
/* Every entry point must reject an empty helper set and leave its output
|
frost_enrollment: implement the three rounds
Third of six commits. Replaces the Phase 1 stubs with the real
arithmetic, adds a smoke test that a 2-of-3 group really does grow into
a working 2-of-4 one, and wires the entry points into ctime_tests.
The Lagrange machinery is frost's, called in place. pubshare_derive is a
skin over secp256k1_frost_derive_pubshare_at
(src/modules/frost/keygen_impl.h:150) evaluated at identifier new_id,
and the id canonicalization is secp256k1_frost_sort_ids, reached through
the declaration the previous commit added.
The one piece frost could not supply is the scalar Lagrange coefficient
at an arbitrary point. frost's secp256k1_frost_derive_interpolating_value
evaluates at x-coordinate 0, which is what reconstructing the group
secret needs; enrollment needs the basis polynomial at the TARGET
x-coordinate. secp256k1_frost_enrollment_lagrange_at is that, and it is
deliberately the same product derive_pubshare_at applies to each
pubshare, in the same identifier space -- so the scalar path and the
point path agree by construction rather than by coincidence. Working in
identifier space is what makes the id-to-x-coordinate +1 cancel: an
x-coordinate difference x_j - x_i is the identifier difference
id_j - id_i.
Round 1.1 computes v = lambda * secshare and splits it. Every share but
the one kept locally is masking randomness derived as
Scalar.from_bytes_wrapping(
TH("FROST enrollment/share_split",
rand32 || params_hash32 || ser32(my_id) || ser32(recipient_id)))
with rand32 = TH(same tag, session_secrand32) XOR secshare32; the kept
share absorbs the remainder so the set sums to v. Three details:
- The reduction wraps rather than rejects, chilldkg's
from_bytes_wrapping (src/modules/chilldkg/util_impl.h:394). A 256-bit
hash mod the group order is about 2^-128 from uniform; rejection
sampling would buy that back in exchange for a variable-time loop.
- Masking with the secret share is the secp256k1_frost_nonce_gen pattern
(session_impl.h:340), so a broken RNG alone does not reveal the split.
- The derivation is indexed by the recipient's IDENTIFIER, not by its
position in the caller's ids array. The plan called for a counter;
identifiers are unique, so they are one, and using them makes the
split independent of the order a caller lists the helper set in. What
the binding buys is DOMAIN SEPARATION only: params_hash32 carries the
group key and the whole parameter tuple, so two runs sharing a seed
but differing in either cannot produce the same deltas. It cannot
detect a disagreement between helpers, because nothing cross-checks
per-helper private randomness. That is the params hash's job.
session_secrand32 is wiped whether the call succeeds or fails, so a
caller cannot retry a failed run on the same randomness.
Round 1.2 recomputes its own params hash from the group key and the
tuple, compares every received hash against it, then sums. The slot at
the caller's own position in received_params_hashes32 is skipped, while
the same position in all_shares32 is read -- the asymmetry the header
documents, and the thing that makes this a recomputation rather than a
string comparison. The mode and bounds are re-validated here rather than
trusted from the round 1.1 call site, since the full tuple is present.
An out-of-range share is reported through mismatch_id the way
secp256k1_frost_partial_sig_agg reports an unparseable partial
signature.
Round 2 compares the params hash against its own recomputation over the
authenticated group key, sums, rejects a zero share, and checks
secshare*G against the expected public share.
Three deviations from the plan, all to match what the tree already does:
- Value ranges return 0; only pointers get ARG_CHECK. The plan called
for an ARG_CHECK on the n_ids bound, but the frost module's split is
the one used here (secp256k1_frost_trusted_dealer_keygen,
keygen_impl.h:227), and the header already documents these as
return-0 conditions. The bound is still enforced in production builds
-- params_are_valid requires 2 <= threshold <= n_ids <= n_participants
<= 128 -- so it does not ride on the VERIFY_CHECK inside
secp256k1_frost_sort_ids, which is what the plan was guarding against.
- The public-share check declassifies the derived point and compares
with secp256k1_ge_eq_var, rather than comparing 33 serialized bytes in
constant time. There is no constant-time memcmp in this tree, and
secshare*G is a public key: secp256k1_frost_sign declassifies exactly
this quantity before exactly this comparison
(src/modules/frost/session_impl.h:770, :789). Inventing a primitive to
avoid following that precedent would be the worse trade.
- params_hash's public entry point delegates to the same internal
routine every gate uses, so the encoding has exactly one
implementation to keep in step with the vectors.
One real bug found by the tooling rather than by reading. Accumulators
were initialized with secp256k1_scalar_clear, and
secp256k1_memclear_explicit marks its target UNDEFINED in VERIFY builds
(src/util.h:295) precisely so that reading cleared memory is caught. It
was: valgrind reported 143752 errors in share_agg's summation loop.
Accumulators now start at secp256k1_scalar_set_int(x, 0); scalar_clear
is used only where it means "done with this secret". Worth stating
plainly because the failure mode is invisible in a production build,
where memclear_explicit only zeroes.
ctime_tests gains a 2-of-3-enrolls-a-fourth block covering all three
rounds, following prefractal's b66c757b. The threshold key, the secret
shares, the session randomness and every delta and sigma on the wire are
marked secret; the identifiers, public shares, group key, parameters
hashes and derived public share are not. Under valgrind: 0 errors from 0
contexts, so no branch or memory access in the new code depends on
secret data.
Verification: ./tests, ./noverify_tests and ./exhaustive_tests exit 0;
the frost_enrollment module runs clean under valgrind (0 errors); a
separate CPPFLAGS='-DVERIFY' build compiles without warnings and passes;
the module builds warning-free alongside frost, chilldkg, iceberg and
prefractal.
The smoke test is the substantive check: after a 2-of-3 group enrolls
participant 3, every pair {i, 3} for i in 0..2 reconstructs the original
threshold secret and matches the threshold public key, and the untouched
pair {0, 1} still does too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:06:59 +02:00
|
|
|
* zeroed. This also checks that all five symbols are reachable from the test
|
|
|
|
|
* binary. */
|
build: wire the frost_enrollment module into both build systems
Second of six commits adding the frost_enrollment module. This one is
scaffolding only: the five entry points are stubs that validate their
pointer arguments, zero their outputs and return 0. What is being
verified here is that the module configures, compiles, links, exports
its symbols and registers its test module in both build systems -- so
that the next commit changes nothing but arithmetic.
Ordering is the one thing in this commit that can go silently wrong, and
it goes wrong in opposite directions in the two build systems:
- configure.ac executes its `if` blocks in file order, and
enable_module_frost defaults to no (configure.ac:243). A block placed
after the frost block at :601 that sets enable_module_frost=yes flips
the variable too late: AM_CONDITIONAL goes true, so the header is
installed and the Makefile fragment is pulled in, but
-DENABLE_MODULE_FROST=1 is never appended, so src/secp256k1.c never
includes frost's implementation and every secp256k1_frost_* symbol
fails to link. The new block therefore goes ahead of both the frost
block and prefractal's, which documents the same trap.
- src/CMakeLists.txt processes dependents FIRST, so the same block goes
above the FROST block there, beside prefractal's.
Verified rather than assumed: configuring with ONLY
--enable-module-frost-enrollment emits -DENABLE_MODULE_FROST=1
alongside -DENABLE_MODULE_FROST_ENROLLMENT=1, and the CMake summary
prints "frost ON" for the same configuration -- the latter is what the
PARENT_SCOPE lift buys, since the summary runs after
add_subdirectory(src) and would otherwise report a module it is
compiling in as OFF.
The dependency guard is prefractal's implies-frost idiom, copied
verbatim along with its reasoning. frost is default-OFF, so the
`test x"$enable_module_frost" = x"no"` / `DEFINED X AND NOT X` guard
every other module uses -- which reads as "the user disabled it
explicitly" for a default-ON dependency -- is true by default here and
cannot tell an explicit --disable-module-frost from the default once
both are in the cache. Enabling frost-enrollment simply implies frost,
with no error.
The one frost-module change in the whole series is in this commit:
src/modules/frost/session.h gains a declaration for
secp256k1_frost_sort_ids, which is defined at session_impl.h:517 and
declared nowhere. The params hash needs it to canonicalize identifier
order. Prefractal reaches frost's statics through translation-unit
ordering alone; rather than inherit reuse-by-link-order, this declares
the function where keygen.h:48 already declares derive_pubshare_at, so
the reuse goes through an interface. No behavior change: it is a
declaration for an existing static definition in the same TU.
CI wiring is two files, and skipping either half fails quietly:
- ci/ci.sh gets FROST_ENROLLMENT in the reproduction header's variable
list and --enable-module-frost-enrollment="$FROST_ENROLLMENT" after
the prefractal line.
- .github/workflows/ci.yml gets FROST_ENROLLMENT at every PREFRACTAL
site: the global default, 11 inline matrix entries and 10 job-level
env blocks. Without the default, ci.sh runs under set -eux with an
empty $FROST_ENROLLMENT, passes --enable-module-frost-enrollment="",
`test x"" = x"yes"` is false, and the module is off in all of CI while
ci.sh visibly has the plumbing.
Verified programmatically over the parsed workflow: across the 106
effective job contexts, PREFRACTAL and FROST_ENROLLMENT now agree in
every single one (45 set to yes, no mismatches), no context sets
FROST_ENROLLMENT without FROST or without EXPERIMENTAL, and no context
leaves it undefined. ci.sh passes sh -n.
The stub test is not a placeholder that has to be deleted later: every
entry point must reject an empty helper set and leave its output zeroed,
which is true of the stubs and stays true of the finished
implementation, so it doubles as the check that all five symbols are
reachable from the test binary.
Verification. Autotools: ./autogen.sh, then a frost-enrollment-only
configure and a full configure with frost, chilldkg, iceberg, prefractal
and frost-enrollment all on -- both build with zero warnings under the
project's -Werror-grade flag set, ./tests and ./exhaustive_tests exit 0,
and `./tests -l` lists the frost_enrollment module. CMake: configure with
-DSECP256K1_EXPERIMENTAL=ON -DSECP256K1_ENABLE_MODULE_FROST_ENROLLMENT=ON
builds clean and ctest passes 391 tests. nm shows the five new symbols
exported from libsecp256k1.so; tools/symbol-check.py could not be run
here because python3-lief is not installed in this environment, but all
five carry the required secp256k1_ prefix. make dist succeeds and the
tarball carries src/modules/frost_enrollment/frost_enrollment.md
alongside the other module documents.
One unrelated observation from this build: a stale
src/ctime_tests-ctime_tests.o left over from an earlier configure with a
different module set will fail to link, because automake does not track
CPPFLAGS changes across reconfigures. make clean between configurations
with different module sets, not a fault in this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 03:53:03 +02:00
|
|
|
static void run_frost_enrollment_rejects_empty_set_test(void) {
|
|
|
|
|
secp256k1_pubkey pk;
|
|
|
|
|
secp256k1_pubkey pubshare;
|
|
|
|
|
unsigned char buf32[32];
|
|
|
|
|
unsigned char secrand32[32];
|
|
|
|
|
unsigned char hash32[32];
|
|
|
|
|
uint32_t ids[1] = { 0 };
|
|
|
|
|
|
|
|
|
|
memset(&pk, 0, sizeof(pk));
|
|
|
|
|
memset(&pubshare, 0xff, sizeof(pubshare));
|
|
|
|
|
memset(secrand32, 0x11, sizeof(secrand32));
|
|
|
|
|
|
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, buf32, &pk, ids, 0, 0, 1, 2) == 0);
|
|
|
|
|
CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32)));
|
|
|
|
|
|
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
|
|
|
memset(hash32, 0xff, sizeof(hash32));
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, buf32, hash32, secrand32, buf32, &pk, ids, 0, 0, 1, 1, 2) == 0);
|
|
|
|
|
CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32)));
|
|
|
|
|
|
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, buf32, NULL, buf32, hash32, &pk, ids, 0, 0, 1, 1, 2) == 0);
|
|
|
|
|
CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32)));
|
|
|
|
|
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, &pk, ids, 0, 0, 1, 2) == 0);
|
|
|
|
|
CHECK(secp256k1_is_zero_array((unsigned char *)&pubshare, sizeof(pubshare)));
|
|
|
|
|
|
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
|
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, buf32, buf32, &pk, ids, 0, 0, 1, 2, NULL, NULL) == 0);
|
|
|
|
|
CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct tf_test_entry tests_frost_enrollment[] = {
|
frost_enrollment: implement the three rounds
Third of six commits. Replaces the Phase 1 stubs with the real
arithmetic, adds a smoke test that a 2-of-3 group really does grow into
a working 2-of-4 one, and wires the entry points into ctime_tests.
The Lagrange machinery is frost's, called in place. pubshare_derive is a
skin over secp256k1_frost_derive_pubshare_at
(src/modules/frost/keygen_impl.h:150) evaluated at identifier new_id,
and the id canonicalization is secp256k1_frost_sort_ids, reached through
the declaration the previous commit added.
The one piece frost could not supply is the scalar Lagrange coefficient
at an arbitrary point. frost's secp256k1_frost_derive_interpolating_value
evaluates at x-coordinate 0, which is what reconstructing the group
secret needs; enrollment needs the basis polynomial at the TARGET
x-coordinate. secp256k1_frost_enrollment_lagrange_at is that, and it is
deliberately the same product derive_pubshare_at applies to each
pubshare, in the same identifier space -- so the scalar path and the
point path agree by construction rather than by coincidence. Working in
identifier space is what makes the id-to-x-coordinate +1 cancel: an
x-coordinate difference x_j - x_i is the identifier difference
id_j - id_i.
Round 1.1 computes v = lambda * secshare and splits it. Every share but
the one kept locally is masking randomness derived as
Scalar.from_bytes_wrapping(
TH("FROST enrollment/share_split",
rand32 || params_hash32 || ser32(my_id) || ser32(recipient_id)))
with rand32 = TH(same tag, session_secrand32) XOR secshare32; the kept
share absorbs the remainder so the set sums to v. Three details:
- The reduction wraps rather than rejects, chilldkg's
from_bytes_wrapping (src/modules/chilldkg/util_impl.h:394). A 256-bit
hash mod the group order is about 2^-128 from uniform; rejection
sampling would buy that back in exchange for a variable-time loop.
- Masking with the secret share is the secp256k1_frost_nonce_gen pattern
(session_impl.h:340), so a broken RNG alone does not reveal the split.
- The derivation is indexed by the recipient's IDENTIFIER, not by its
position in the caller's ids array. The plan called for a counter;
identifiers are unique, so they are one, and using them makes the
split independent of the order a caller lists the helper set in. What
the binding buys is DOMAIN SEPARATION only: params_hash32 carries the
group key and the whole parameter tuple, so two runs sharing a seed
but differing in either cannot produce the same deltas. It cannot
detect a disagreement between helpers, because nothing cross-checks
per-helper private randomness. That is the params hash's job.
session_secrand32 is wiped whether the call succeeds or fails, so a
caller cannot retry a failed run on the same randomness.
Round 1.2 recomputes its own params hash from the group key and the
tuple, compares every received hash against it, then sums. The slot at
the caller's own position in received_params_hashes32 is skipped, while
the same position in all_shares32 is read -- the asymmetry the header
documents, and the thing that makes this a recomputation rather than a
string comparison. The mode and bounds are re-validated here rather than
trusted from the round 1.1 call site, since the full tuple is present.
An out-of-range share is reported through mismatch_id the way
secp256k1_frost_partial_sig_agg reports an unparseable partial
signature.
Round 2 compares the params hash against its own recomputation over the
authenticated group key, sums, rejects a zero share, and checks
secshare*G against the expected public share.
Three deviations from the plan, all to match what the tree already does:
- Value ranges return 0; only pointers get ARG_CHECK. The plan called
for an ARG_CHECK on the n_ids bound, but the frost module's split is
the one used here (secp256k1_frost_trusted_dealer_keygen,
keygen_impl.h:227), and the header already documents these as
return-0 conditions. The bound is still enforced in production builds
-- params_are_valid requires 2 <= threshold <= n_ids <= n_participants
<= 128 -- so it does not ride on the VERIFY_CHECK inside
secp256k1_frost_sort_ids, which is what the plan was guarding against.
- The public-share check declassifies the derived point and compares
with secp256k1_ge_eq_var, rather than comparing 33 serialized bytes in
constant time. There is no constant-time memcmp in this tree, and
secshare*G is a public key: secp256k1_frost_sign declassifies exactly
this quantity before exactly this comparison
(src/modules/frost/session_impl.h:770, :789). Inventing a primitive to
avoid following that precedent would be the worse trade.
- params_hash's public entry point delegates to the same internal
routine every gate uses, so the encoding has exactly one
implementation to keep in step with the vectors.
One real bug found by the tooling rather than by reading. Accumulators
were initialized with secp256k1_scalar_clear, and
secp256k1_memclear_explicit marks its target UNDEFINED in VERIFY builds
(src/util.h:295) precisely so that reading cleared memory is caught. It
was: valgrind reported 143752 errors in share_agg's summation loop.
Accumulators now start at secp256k1_scalar_set_int(x, 0); scalar_clear
is used only where it means "done with this secret". Worth stating
plainly because the failure mode is invisible in a production build,
where memclear_explicit only zeroes.
ctime_tests gains a 2-of-3-enrolls-a-fourth block covering all three
rounds, following prefractal's b66c757b. The threshold key, the secret
shares, the session randomness and every delta and sigma on the wire are
marked secret; the identifiers, public shares, group key, parameters
hashes and derived public share are not. Under valgrind: 0 errors from 0
contexts, so no branch or memory access in the new code depends on
secret data.
Verification: ./tests, ./noverify_tests and ./exhaustive_tests exit 0;
the frost_enrollment module runs clean under valgrind (0 errors); a
separate CPPFLAGS='-DVERIFY' build compiles without warnings and passes;
the module builds warning-free alongside frost, chilldkg, iceberg and
prefractal.
The smoke test is the substantive check: after a 2-of-3 group enrolls
participant 3, every pair {i, 3} for i in 0..2 reconstructs the original
threshold secret and matches the threshold public key, and the untouched
pair {0, 1} still does too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:06:59 +02:00
|
|
|
CASE1(run_frost_enrollment_smoke_test),
|
build: wire the frost_enrollment module into both build systems
Second of six commits adding the frost_enrollment module. This one is
scaffolding only: the five entry points are stubs that validate their
pointer arguments, zero their outputs and return 0. What is being
verified here is that the module configures, compiles, links, exports
its symbols and registers its test module in both build systems -- so
that the next commit changes nothing but arithmetic.
Ordering is the one thing in this commit that can go silently wrong, and
it goes wrong in opposite directions in the two build systems:
- configure.ac executes its `if` blocks in file order, and
enable_module_frost defaults to no (configure.ac:243). A block placed
after the frost block at :601 that sets enable_module_frost=yes flips
the variable too late: AM_CONDITIONAL goes true, so the header is
installed and the Makefile fragment is pulled in, but
-DENABLE_MODULE_FROST=1 is never appended, so src/secp256k1.c never
includes frost's implementation and every secp256k1_frost_* symbol
fails to link. The new block therefore goes ahead of both the frost
block and prefractal's, which documents the same trap.
- src/CMakeLists.txt processes dependents FIRST, so the same block goes
above the FROST block there, beside prefractal's.
Verified rather than assumed: configuring with ONLY
--enable-module-frost-enrollment emits -DENABLE_MODULE_FROST=1
alongside -DENABLE_MODULE_FROST_ENROLLMENT=1, and the CMake summary
prints "frost ON" for the same configuration -- the latter is what the
PARENT_SCOPE lift buys, since the summary runs after
add_subdirectory(src) and would otherwise report a module it is
compiling in as OFF.
The dependency guard is prefractal's implies-frost idiom, copied
verbatim along with its reasoning. frost is default-OFF, so the
`test x"$enable_module_frost" = x"no"` / `DEFINED X AND NOT X` guard
every other module uses -- which reads as "the user disabled it
explicitly" for a default-ON dependency -- is true by default here and
cannot tell an explicit --disable-module-frost from the default once
both are in the cache. Enabling frost-enrollment simply implies frost,
with no error.
The one frost-module change in the whole series is in this commit:
src/modules/frost/session.h gains a declaration for
secp256k1_frost_sort_ids, which is defined at session_impl.h:517 and
declared nowhere. The params hash needs it to canonicalize identifier
order. Prefractal reaches frost's statics through translation-unit
ordering alone; rather than inherit reuse-by-link-order, this declares
the function where keygen.h:48 already declares derive_pubshare_at, so
the reuse goes through an interface. No behavior change: it is a
declaration for an existing static definition in the same TU.
CI wiring is two files, and skipping either half fails quietly:
- ci/ci.sh gets FROST_ENROLLMENT in the reproduction header's variable
list and --enable-module-frost-enrollment="$FROST_ENROLLMENT" after
the prefractal line.
- .github/workflows/ci.yml gets FROST_ENROLLMENT at every PREFRACTAL
site: the global default, 11 inline matrix entries and 10 job-level
env blocks. Without the default, ci.sh runs under set -eux with an
empty $FROST_ENROLLMENT, passes --enable-module-frost-enrollment="",
`test x"" = x"yes"` is false, and the module is off in all of CI while
ci.sh visibly has the plumbing.
Verified programmatically over the parsed workflow: across the 106
effective job contexts, PREFRACTAL and FROST_ENROLLMENT now agree in
every single one (45 set to yes, no mismatches), no context sets
FROST_ENROLLMENT without FROST or without EXPERIMENTAL, and no context
leaves it undefined. ci.sh passes sh -n.
The stub test is not a placeholder that has to be deleted later: every
entry point must reject an empty helper set and leave its output zeroed,
which is true of the stubs and stays true of the finished
implementation, so it doubles as the check that all five symbols are
reachable from the test binary.
Verification. Autotools: ./autogen.sh, then a frost-enrollment-only
configure and a full configure with frost, chilldkg, iceberg, prefractal
and frost-enrollment all on -- both build with zero warnings under the
project's -Werror-grade flag set, ./tests and ./exhaustive_tests exit 0,
and `./tests -l` lists the frost_enrollment module. CMake: configure with
-DSECP256K1_EXPERIMENTAL=ON -DSECP256K1_ENABLE_MODULE_FROST_ENROLLMENT=ON
builds clean and ctest passes 391 tests. nm shows the five new symbols
exported from libsecp256k1.so; tools/symbol-check.py could not be run
here because python3-lief is not installed in this environment, but all
five carry the required secp256k1_ prefix. make dist succeeds and the
tarball carries src/modules/frost_enrollment/frost_enrollment.md
alongside the other module documents.
One unrelated observation from this build: a stale
src/ctime_tests-ctime_tests.o left over from an earlier configure with a
different module set will fail to link, because automake does not track
CPPFLAGS changes across reconfigures. make clean between configurations
with different module sets, not a fault in this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 03:53:03 +02:00
|
|
|
CASE1(run_frost_enrollment_rejects_empty_set_test),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#endif /* SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H */
|