Files
secp256k1-zkp/examples/frost_enrollment.c
Kgothatso Ngako 3c962b00cc frost_enrollment: fix the example's stale n and failure-path hygiene
The last of the review findings, plus the comment and structure fixes it
listed.

The example's repair run used the pre-enrollment participant count.

  Two blocks earlier the example teaches that every participant must
  update its record of n from 3 to 4 after an enrollment, and the
  signing session duly uses N_PARTICIPANTS_AFTER. Then enroll() -- which
  hard-coded N_PARTICIPANTS -- ran the repair at n = 3. It worked only
  because the Lagrange math never involves n and every party in this
  single-process demo passed the same stale value.

  In a real post-enrollment repair it would not. n is bound into the
  parameters hash, so helpers feeding their updated n = 4 into
  shares_gen while the requester feeds n = 3 abort round 1.2 with no
  visible cause. enroll() now takes n_participants as a parameter, the
  repair passes N_PARTICIPANTS_AFTER, and both the function's contract
  comment and the repair call site say why. The repaired share is still
  byte-identical to the original, which is the point: n changes the
  hash, not the arithmetic.

The example leaked secrets on its failure paths.

  enroll() erased the delta and sigma buffers only on success; four
  early returns left them live. sign_and_verify() returned from three
  places without erasing already-generated secnonces. Both now route
  every exit through a cleanup block. This example is otherwise more
  careful about erasure than its siblings, so the asymmetry was exactly
  what a reader copying it would carry into production -- on the fault
  paths where hygiene matters most.

  The double-wipe of session_secrand is gone with it: shares_gen and
  nonce_gen both wipe the seed on every path, and doing it again read
  as uncertainty about the contract. The comment now states the
  contract instead. The fill_random failure path does erase, since
  nothing else has touched the buffer there.

The example's mismatch message asserted a cause it cannot know.

  It printed "Helper %u disagrees about the enrollment parameters" for
  what may equally be a corrupted share, per the previous commit's
  finding. It now says the helper "contributed a share this helper
  cannot use", with a comment noting that share_agg does not
  distinguish the two causes so neither can the message.

Comment and structure fixes, all noted in the review:

- The vector generator claimed case 4 was "the only case whose DERIVED
  public share has odd Y". It is not -- cases 1, 2 and 4 are odd and
  case 3 is even. The comment existed to justify a coverage choice and
  misinformed; both parity comments now describe the set accurately and
  say they document it rather than constrain it. Regenerating vectors.h
  still reproduces it byte for byte.
- The secp256k1_frost_sort_ids declaration in frost/session.h no longer
  duplicates the definition's doc comment, which was two copies to keep
  in sync. It says what the function is for and points at the
  definition for the contract.
- The t >= 2 rationale was stated in full in three places. The impl now
  states the conclusion and names frost_enrollment.md as the single
  place to edit if the policy moves.
- The ctime_tests comments read ambiguously ("the parameters hash is
  public, the delta values are not" against a header calling deltas
  secret), and computing direct_hash without asserting anything invited
  a "forgotten assertion" reading. Both are now explicit.
- The example moves next to frost_example in Makefile.am rather than
  after iceberg, matching the FROST-stack grouping used in
  configure.ac, ci.sh, ci.yml and README.
- frost_enrollment.md now distinguishes what is unstable (the C API)
  from what is frozen (the wire-visible encodings), which the two
  statements together previously left easy to conflate.

Not fixed, deliberately, and now recorded where the tree can see it: the
plan called for a CHANGELOG.md entry. That file states in its first two
lines that it is upstream libsecp256k1's changelog and not this fork's,
and none of frost, chilldkg, iceberg or prefractal has an entry. Adding
the first one is a decision about all five modules, not this one. The
README link is the fork's actual convention for announcing a module and
is in place.

Verification: autotools builds warning-free and `make check` is 12/12
including the example; ctime_tests is clean under valgrind; `make dist`
carries all nine frost_enrollment files; CMake with examples builds
warning-free and ctest is 542/542; the example source is clean under
gcc -std=c89 -pedantic -Wall -Wextra; regenerating vectors.h reproduces
it byte for byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:27:17 +02:00

460 lines
21 KiB
C

/*************************************************************************
* To the extent possible under law, the author(s) have dedicated all *
* copyright and related and neighboring rights to the software in this *
* file to the public domain worldwide. This software is distributed *
* without any warranty. For the CC0 Public Domain Dedication, see *
* EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 *
*************************************************************************/
/** This file demonstrates how to use the FROST enrollment module: first to
* grow a 2-of-3 group into a 2-of-4 one without re-running key generation,
* then to repair a participant's lost share. Additionally, see the
* documentation in include/secp256k1_frost_enrollment.h and
* src/modules/frost_enrollment/frost_enrollment.md.
*
* The example runs all roles (trusted dealer, helpers, the party receiving
* the share, coordinator) in a single process. In a real deployment these
* roles are performed by different parties, and the enrollment shares and
* the aggregated values they produce MUST travel over confidential and
* authenticated channels: they are additive shares of real secret shares.
*
* Two things this example demonstrates that are easy to get wrong:
*
* 1. The verification flow is only non-circular if the threshold public key
* comes from somewhere the helpers do not control. Here the enrollee gets
* it straight from the dealer step, which stands in for real
* authentication, and validates the helpers' public shares against it
* before deriving anything from them.
* 2. Nothing in the protocol decides WHO may be enrolled. That is a
* caller-side precondition; see the note before enroll() below.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
#include <secp256k1_frost.h>
#include <secp256k1_frost_enrollment.h>
#include <secp256k1_schnorrsig.h>
#include "examples_util.h"
/* Number of participants n before the enrollment */
#define N_PARTICIPANTS 3
/* Number of participants after the enrollment */
#define N_PARTICIPANTS_AFTER (N_PARTICIPANTS + 1)
/* Threshold t */
#define THRESHOLD 2
/* Number of helpers u taking part in the enrollment run. Any u with
* THRESHOLD <= u <= N_PARTICIPANTS works; the result does not depend on the
* choice. */
#define N_HELPERS 2
/* The identifier the new participant will hold. In enrollment mode this must
* be exactly the current participant count. */
#define NEW_ID N_PARTICIPANTS
/* The signers of the demonstration signing session: participant 2 and the
* newly enrolled participant. */
#define N_SIGNERS 2
/* What one helper holds during a run. */
struct helper {
uint32_t id;
/* The helper's own secret share of the group key. Never leaves the
* helper. */
unsigned char secshare[32];
/* Round 1.1 output, aligned with the helper id array: entry j goes to the
* helper at position j. The entry at this helper's own position is kept
* locally. These are secret. */
unsigned char shares[N_HELPERS * 32];
/* The parameters hash this helper computed, sent alongside every share it
* distributes and to the enrollee. Public. */
unsigned char params_hash[32];
/* Round 1.2 output, sent to the enrollee. Secret. */
unsigned char sigma[32];
};
/* Run the trusted dealer key generation for the initial 2-of-3 group.
*
* WARNING: The trusted dealer knows the threshold secret key and all secret
* shares, and must erase them after distributing the shares. A distributed key
* generation protocol avoids a trusted dealer entirely; see the chilldkg
* module. */
static int trusted_dealer_keygen(const secp256k1_context* ctx, unsigned char *threshold_seckey, unsigned char *secshares, secp256k1_pubkey *thresh_pk, secp256k1_pubkey *pubshares) {
if (!fill_random(threshold_seckey, 32)) {
printf("Failed to generate randomness\n");
return 0;
}
if (!secp256k1_frost_trusted_dealer_keygen(ctx, secshares, thresh_pk, pubshares, N_PARTICIPANTS, THRESHOLD, threshold_seckey)) {
return 0;
}
return secp256k1_frost_threshold_info_validate(ctx, thresh_pk, pubshares, N_PARTICIPANTS, THRESHOLD);
}
/* Run one enrollment or repair, all three rounds.
*
* `helper_pubshares` and `helpers` are aligned with `ids`. On success the new
* secret share is written to `new_secshare` and its public counterpart to
* `new_pubshare`.
*
* `n_participants` must be the group size all parties CURRENTLY agree on: it
* is bound into the parameters hash, so a helper using a stale n and one
* using the updated n abort round 1.2 against each other. That is why this is
* a parameter and not the N_PARTICIPANTS constant -- the repair below runs
* after the enrollment, when the group has already grown to
* N_PARTICIPANTS_AFTER.
*
* PRECONDITION THE LIBRARY CANNOT ENFORCE: the helpers must already have
* agreed, out of band, that this party is entitled to a share at `new_id`.
* The protocol has no authorization step of its own: anyone who convinces t
* helpers to run it walks away with a valid share, and in repair mode that is
* an existing participant's actual share. Authenticated channels establish who
* is speaking, not that the group approved the request. */
static int enroll(const secp256k1_context* ctx, unsigned char *new_secshare, secp256k1_pubkey *new_pubshare, struct helper *helpers, const uint32_t *ids, const secp256k1_pubkey *helper_pubshares, const secp256k1_pubkey *thresh_pk, uint32_t new_id, size_t n_participants) {
unsigned char sigmas[N_HELPERS * 32];
unsigned char all_shares[N_HELPERS * 32];
unsigned char enrollee_params_hash[32];
int i, j;
int ret = 0;
memset(sigmas, 0, sizeof(sigmas));
memset(all_shares, 0, sizeof(all_shares));
/* --- Round 1.1 ------------------------------------------------------
* Every helper splits its Lagrange-weighted share into one additive share
* per helper, keeps its own and sends the rest out. */
for (i = 0; i < N_HELPERS; i++) {
unsigned char session_secrand[32];
/* Fresh randomness for every run: reusing it across runs leaks share
* information. No secure_erase is needed here -- shares_gen wipes the
* seed itself, on success and on failure alike, so that a failed call
* cannot be retried on the same randomness. */
if (!fill_random(session_secrand, sizeof(session_secrand))) {
printf("Failed to generate randomness\n");
secure_erase(session_secrand, sizeof(session_secrand));
goto cleanup;
}
if (!secp256k1_frost_enrollment_shares_gen(ctx, helpers[i].shares, helpers[i].params_hash, session_secrand, helpers[i].secshare, thresh_pk, ids, N_HELPERS, helpers[i].id, new_id, n_participants, THRESHOLD)) {
/* shares_gen wipes the seed on every path, including this one. */
goto cleanup;
}
}
/* --- Round 1.2 ------------------------------------------------------
* Helper j receives entry j of every other helper's buffer, over a
* confidential and authenticated channel, together with that helper's
* parameters hash. It recomputes the hash itself and compares. */
for (j = 0; j < N_HELPERS; j++) {
unsigned char received_hashes[N_HELPERS * 32];
uint32_t mismatch_id;
/* The two buffers take deliberately opposite conventions at this
* helper's own position: the share kept locally IS read, while the
* hash slot is never read and stays zero. The own hash is
* recomputed, never taken from a buffer -- which is what makes this
* a recomputation check rather than a comparison between two strings
* the caller supplied. */
memset(received_hashes, 0, sizeof(received_hashes));
for (i = 0; i < N_HELPERS; i++) {
memcpy(&all_shares[32 * i], &helpers[i].shares[32 * j], 32);
if (i != j) {
memcpy(&received_hashes[32 * i], helpers[i].params_hash, 32);
}
}
if (!secp256k1_frost_enrollment_share_agg(ctx, helpers[j].sigma, &mismatch_id, all_shares, received_hashes, thresh_pk, ids, N_HELPERS, helpers[j].id, new_id, n_participants, THRESHOLD)) {
if (mismatch_id != UINT32_MAX) {
/* Either that helper ran round 1.1 on different parameters,
* or its share did not survive transit. share_agg does not
* distinguish the two, so neither can this message. */
printf("\nHelper %u contributed a share this helper cannot use\n", mismatch_id);
}
goto cleanup;
}
}
/* --- Round 2 --------------------------------------------------------
* The enrollee receives one value from each helper, again confidentially
* and authenticated, plus the parameters hash.
*
* The expected public share is derived from the helpers' public shares --
* which the caller has already validated against an independently
* authenticated threshold public key (see main). Without that step both
* checks below would be circular: t colluding helpers could present a
* consistent but fabricated polynomial and every check would pass on a
* worthless share. */
for (i = 0; i < N_HELPERS; i++) {
memcpy(&sigmas[32 * i], helpers[i].sigma, 32);
}
memcpy(enrollee_params_hash, helpers[0].params_hash, 32);
if (!secp256k1_frost_enrollment_pubshare_derive(ctx, new_pubshare, helper_pubshares, ids, N_HELPERS, new_id, n_participants, THRESHOLD)) {
goto cleanup;
}
if (!secp256k1_frost_enrollment_secshare_gen(ctx, new_secshare, sigmas, thresh_pk, ids, N_HELPERS, new_id, n_participants, THRESHOLD, enrollee_params_hash, new_pubshare)) {
goto cleanup;
}
ret = 1;
cleanup:
/* Every exit runs this, not just the successful one. The delta and sigma
* values are additive shares of real secret shares, and the paths where
* hygiene matters most are exactly the ones a protocol fault takes. */
secure_erase(sigmas, sizeof(sigmas));
secure_erase(all_shares, sizeof(all_shares));
for (i = 0; i < N_HELPERS; i++) {
secure_erase(helpers[i].shares, sizeof(helpers[i].shares));
secure_erase(helpers[i].sigma, sizeof(helpers[i].sigma));
}
return ret;
}
/* Produce a BIP340 signature with the given signer set and verify it against
* the (untweaked) threshold public key. `secshares` and `pubshares` are
* aligned with `ids`. `n_participants` is the group size AFTER the enrollment,
* which every participant must have updated consistently. */
static int sign_and_verify(const secp256k1_context* ctx, const uint32_t *ids, unsigned char (*secshares)[32], const secp256k1_pubkey *pubshares, const secp256k1_pubkey *thresh_pk, size_t n_participants, const unsigned char *msg, size_t msglen) {
secp256k1_frost_tweak_cache cache;
secp256k1_frost_secnonce secnonces[N_SIGNERS];
secp256k1_frost_pubnonce pubnonces[N_SIGNERS];
const secp256k1_frost_pubnonce *pubnonce_ptrs[N_SIGNERS];
secp256k1_frost_partial_sig partial_sigs[N_SIGNERS];
const secp256k1_frost_partial_sig *partial_sig_ptrs[N_SIGNERS];
secp256k1_frost_aggnonce aggnonce;
secp256k1_frost_session session;
secp256k1_xonly_pubkey tweaked_pk;
unsigned char tweaked_pk32[32];
unsigned char sig[64];
int i;
int ret = 0;
/* Zeroed up front so the cleanup below can erase them unconditionally,
* whichever failure path got there. */
memset(secnonces, 0, sizeof(secnonces));
if (!secp256k1_frost_tweak_cache_init(ctx, &cache, thresh_pk)) {
goto cleanup;
}
/* No tweaks are applied here, so the "tweaked" key is the threshold public
* key itself in its x-only encoding. */
if (!secp256k1_frost_tweaked_pubkey_get(ctx, &tweaked_pk, &cache)
|| !secp256k1_xonly_pubkey_serialize(ctx, tweaked_pk32, &tweaked_pk)) {
goto cleanup;
}
for (i = 0; i < N_SIGNERS; i++) {
unsigned char session_secrand[32];
if (!fill_random(session_secrand, sizeof(session_secrand))) {
printf("Failed to generate randomness\n");
secure_erase(session_secrand, sizeof(session_secrand));
goto cleanup;
}
/* nonce_gen wipes the seed itself, on every path. */
if (!secp256k1_frost_nonce_gen(ctx, &secnonces[i], &pubnonces[i], session_secrand, secshares[i], &pubshares[i], tweaked_pk32, msg, msglen, NULL, 0)) {
goto cleanup;
}
pubnonce_ptrs[i] = &pubnonces[i];
partial_sig_ptrs[i] = &partial_sigs[i];
}
if (!secp256k1_frost_nonce_agg(ctx, &aggnonce, NULL, pubnonce_ptrs, N_SIGNERS)) {
goto cleanup;
}
if (!secp256k1_frost_session_init(ctx, &session, &aggnonce, ids, pubshares, N_SIGNERS, n_participants, THRESHOLD, &cache, msg, msglen)) {
goto cleanup;
}
for (i = 0; i < N_SIGNERS; i++) {
if (!secp256k1_frost_sign(ctx, &partial_sigs[i], &secnonces[i], secshares[i], &session, ids, pubshares, N_SIGNERS, ids[i])) {
goto cleanup;
}
if (!secp256k1_frost_partial_sig_verify(ctx, &partial_sigs[i], &pubnonces[i], &pubshares[i], &session, ids, N_SIGNERS, (size_t)i)) {
goto cleanup;
}
}
if (!secp256k1_frost_partial_sig_agg(ctx, sig, NULL, &session, partial_sig_ptrs, N_SIGNERS)) {
goto cleanup;
}
ret = secp256k1_schnorrsig_verify(ctx, sig, msg, msglen, &tweaked_pk);
cleanup:
/* frost_sign wipes a secnonce it consumed, but a failure before or during
* the signing loop can leave others live. */
for (i = 0; i < N_SIGNERS; i++) {
secure_erase(&secnonces[i], sizeof(secnonces[i]));
}
return ret;
}
int main(void) {
secp256k1_context* ctx;
/* Key material of the initial 2-of-3 group. The dealer erases all of it
* at the end of this function. */
unsigned char threshold_seckey[32];
unsigned char secshares[N_PARTICIPANTS * 32];
secp256k1_pubkey thresh_pk;
secp256k1_pubkey pubshares[N_PARTICIPANTS_AFTER];
/* The enrollment run for the new participant 3. */
struct helper helpers[N_HELPERS];
uint32_t helper_ids[N_HELPERS] = { 0, 1 };
secp256k1_pubkey helper_pubshares[N_HELPERS];
unsigned char new_secshare[32];
secp256k1_pubkey new_pubshare;
/* The repair run for participant 1. */
struct helper repair_helpers[N_HELPERS];
uint32_t repair_ids[N_HELPERS] = { 0, 2 };
secp256k1_pubkey repair_pubshares[N_HELPERS];
unsigned char repaired_secshare[32];
secp256k1_pubkey repaired_pubshare;
/* The signing session that proves the enrolled participant works. */
uint32_t signer_ids[N_SIGNERS] = { 2, NEW_ID };
unsigned char signer_secshares[N_SIGNERS][32];
secp256k1_pubkey signer_pubshares[N_SIGNERS];
unsigned char msg[32] = "this_could_be_the_hash_of_a_msg";
unsigned char buf[33];
size_t outputlen;
int i;
ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
printf("Generating threshold key material for a %d-of-%d group...", THRESHOLD, N_PARTICIPANTS);
fflush(stdout);
if (!trusted_dealer_keygen(ctx, threshold_seckey, secshares, &thresh_pk, pubshares)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("ok\n");
outputlen = sizeof(buf);
if (!secp256k1_ec_pubkey_serialize(ctx, buf, &outputlen, &thresh_pk, SECP256K1_EC_COMPRESSED)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("Threshold public key: ");
print_hex(buf, outputlen);
fflush(stdout);
/* The new participant obtains the threshold public key from a source it
* authenticates INDEPENDENTLY OF THE HELPERS. In this single-process
* example that is the dealer step above; in a real deployment it might be
* a signed group descriptor, an on-chain commitment, or whatever the group
* already trusts to say what it is.
*
* It then validates the helpers' public shares against that key. This is
* the step that makes the round 2 checks worth anything: without it, the
* enrollee would be checking the helpers' arithmetic against numbers the
* helpers also chose, and t colluding helpers could hand it a consistent
* fake polynomial. */
printf("Validating the group's public shares against the authenticated key...");
fflush(stdout);
if (!secp256k1_frost_threshold_info_validate(ctx, &thresh_pk, pubshares, N_PARTICIPANTS, THRESHOLD)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("ok\n");
/* Enrollment: helpers 0 and 1 give participant 3 a share. */
for (i = 0; i < N_HELPERS; i++) {
helpers[i].id = helper_ids[i];
memcpy(helpers[i].secshare, &secshares[32 * helper_ids[i]], 32);
helper_pubshares[i] = pubshares[helper_ids[i]];
}
printf("Enrolling participant %d with %d helpers...", NEW_ID, N_HELPERS);
fflush(stdout);
if (!enroll(ctx, new_secshare, &new_pubshare, helpers, helper_ids, helper_pubshares, &thresh_pk, NEW_ID, N_PARTICIPANTS)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("ok\n");
/* n -> n+1 bookkeeping. Every participant must extend its table of public
* shares and update its record of n; participants that disagree about n
* compute different Lagrange coefficients and cannot sign together. */
pubshares[NEW_ID] = new_pubshare;
printf("Validating the extended %d-participant key material...", N_PARTICIPANTS_AFTER);
fflush(stdout);
if (!secp256k1_frost_threshold_info_validate(ctx, &thresh_pk, pubshares, N_PARTICIPANTS_AFTER, THRESHOLD)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("ok\n");
outputlen = sizeof(buf);
if (!secp256k1_ec_pubkey_serialize(ctx, buf, &outputlen, &new_pubshare, SECP256K1_EC_COMPRESSED)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("New participant's public share: ");
print_hex(buf, outputlen);
fflush(stdout);
/* Sign with participant 2 and the newly enrolled participant 3. The
* signature verifies against the group's original threshold public key:
* enrollment does not change the group key or any existing share. */
memcpy(signer_secshares[0], &secshares[32 * signer_ids[0]], 32);
memcpy(signer_secshares[1], new_secshare, 32);
signer_pubshares[0] = pubshares[signer_ids[0]];
signer_pubshares[1] = pubshares[signer_ids[1]];
printf("Signing with participants %u and %u...", signer_ids[0], signer_ids[1]);
fflush(stdout);
if (!sign_and_verify(ctx, signer_ids, signer_secshares, signer_pubshares, &thresh_pk, N_PARTICIPANTS_AFTER, msg, sizeof(msg))) {
printf("FAILED\n");
return EXIT_FAILURE;
}
printf("ok\n");
/* Repair: participant 1 lost its share. The same three rounds at
* new_id = 1 reproduce it exactly -- the share is a fixed value, f(x_1),
* not a fresh random one.
*
* Note that n is N_PARTICIPANTS_AFTER here, not N_PARTICIPANTS: the group
* grew above, and n is bound into the parameters hash, so a party still
* using the pre-enrollment value would abort round 1.2 against the
* others. Repair itself does not change n.
*
* This is also the mode where the missing authorization step bites
* hardest: whoever convinces the helpers to run it receives participant
* 1's actual share. */
for (i = 0; i < N_HELPERS; i++) {
repair_helpers[i].id = repair_ids[i];
memcpy(repair_helpers[i].secshare, &secshares[32 * repair_ids[i]], 32);
repair_pubshares[i] = pubshares[repair_ids[i]];
}
printf("Repairing participant 1's lost share...");
fflush(stdout);
if (!enroll(ctx, repaired_secshare, &repaired_pubshare, repair_helpers, repair_ids, repair_pubshares, &thresh_pk, 1, N_PARTICIPANTS_AFTER)) {
printf("FAILED\n");
return EXIT_FAILURE;
}
if (memcmp(repaired_secshare, &secshares[32], 32) != 0) {
printf("FAILED (the repaired share differs from the original)\n");
return EXIT_FAILURE;
}
if (memcmp(&repaired_pubshare, &pubshares[1], sizeof(repaired_pubshare)) != 0) {
printf("FAILED (the repaired public share differs from the original)\n");
return EXIT_FAILURE;
}
printf("ok\n");
/* It's best practice to try to clear secrets from memory after using them.
* This is done because some bugs can allow an attacker to leak memory, for
* example through "out of bounds" array access (see Heartbleed), or the OS
* swapping them to disk. Hence, we overwrite secret key material with
* zeros.
*
* The session randomness was already wiped by shares_gen, and the
* secnonces by frost_sign. */
secure_erase(threshold_seckey, sizeof(threshold_seckey));
secure_erase(secshares, sizeof(secshares));
secure_erase(new_secshare, sizeof(new_secshare));
secure_erase(repaired_secshare, sizeof(repaired_secshare));
secure_erase(signer_secshares, sizeof(signer_secshares));
for (i = 0; i < N_HELPERS; i++) {
secure_erase(&helpers[i], sizeof(helpers[i]));
secure_erase(&repair_helpers[i], sizeof(repair_helpers[i]));
}
secp256k1_context_destroy(ctx);
return EXIT_SUCCESS;
}