Files
secp256k1-zkp/examples/iceberg.c
Kgothatso Ngako 072f6631f3 iceberg: add the example program
Port examples/iceberg.c from the source tree: a full Iceberg session
demonstrating the call order from the module docs -- distributed key
generation, pubshare_gen/pubkey_agg to obtain the group public key,
nonce_gen/nonce_agg into an ordinary MuSig2 public nonce, and
partial_sign/partial_sig_agg into an ordinary MuSig2 partial
signature.

One content adaptation: the secp256k1_musig_nonce_process call gains
a NULL adaptor argument, matching this repo's zkp musig variant.

Wired like the chilldkg example: autotools noinst_PROGRAMS +
TESTS entry under ENABLE_MODULE_ICEBERG (the example runs as part of
make check), CMake example target in examples/CMakeLists.txt, and
iceberg_example added to .gitignore.

Verified: ./iceberg_example runs to completion (exit 0) under both
build systems.
2026-08-31 12:25:09 +02:00

726 lines
33 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 *
*************************************************************************/
/** A 3-of-7 group signing beside an ordinary MuSig2 cosigner.
*
* The group behaves as one MuSig2 participant, and the finished signature is
* an ordinary BIP-340 signature that records nothing about the group.
*
* Three roles appear below and they run different code, which is the thing
* worth keeping straight while reading:
*
* participant holds a share, produces a nonce and a signature share, and
* never sees the whole key. There are seven of them.
* coordinator moves messages around and combines them. Untrusted: every
* check here assumes it is hostile.
* cosigner an ordinary MuSig2 signer that knows nothing about any of
* this and calls the plain musig API.
*
* The participants are wiped between the two rounds and rebuilt from storage,
* because no secret nonce survives that gap; see reboot_participants for what
* does survive it, which is not nothing. One call is made that is expected to
* fail, in refusals_are_refused.
*
* See also include/secp256k1_iceberg.h and doc/iceberg.md.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
#include <secp256k1_schnorrsig.h>
#include <secp256k1_musig.h>
#include <secp256k1_iceberg.h>
/* The trusted dealer lives in its own header, and is not part of the installed
* API. See the note at the top of it. */
#include <secp256k1_iceberg_dealer.h>
#include "examples_util.h"
#define N 7 /* participants in the group */
#define T 3 /* how many of them can sign */
#define MU (2 * T - 1) /* how many must take part in round one */
/* One participant's memory: one secret, and one note to itself.
*
* The share is the secret and never changes. `answered` is the highest session
* label this participant has signed under. The library does not know about it and
* could not, since it holds nothing between calls, but answering twice under one
* label gives away the key share, so somebody has to remember, and the somebody
* is the caller. */
struct participant {
secp256k1_iceberg_share share;
unsigned char answered[32];
};
/* The same participant's disk. A reboot loses the struct above, not this one,
* and both fields have to come back. See reboot_participants. */
struct storage {
unsigned char share_bytes[SECP256K1_ICEBERG_SHARE_MAX_LEN];
size_t share_len;
unsigned char answered_bytes[32];
};
/* The threshold side. Set up once, then unchanged for the life of the group. */
struct group {
struct participant member[N];
struct storage disk[N];
secp256k1_iceberg_pubshare pubshare[N];
secp256k1_pubkey pubkey;
};
/* The other side: an ordinary MuSig2 signer, which knows nothing about any of
* the above. Everything it holds is its own, including the secret nonce that
* the group deliberately does not have an equivalent of. */
struct cosigner {
secp256k1_keypair keypair;
secp256k1_pubkey pubkey;
secp256k1_musig_secnonce secnonce;
};
/* What the two of them add up to. Computed once, at key aggregation, and the
* only thing a verifier ever sees. */
struct shared_key {
secp256k1_musig_keyagg_cache keyagg_cache;
secp256k1_xonly_pubkey output;
};
/* One attempt at one signature. Everything here is public. */
struct signing_session {
unsigned char sid[32];
secp256k1_iceberg_pubnonce contribution[MU];
const secp256k1_iceberg_pubnonce *contribution_ptr[MU];
secp256k1_musig_pubnonce group_nonce; /* the group's, after interpolating */
secp256k1_musig_pubnonce cosigner_nonce; /* the cosigner's, as published */
secp256k1_musig_aggnonce cosigner_aggnonce; /* the cosigners' alone */
secp256k1_musig_session musig_session;
};
static void heading(const char *text) {
printf("\n%s\n", text);
}
static void step(const char *text) {
printf(" %-55s", text);
fflush(stdout);
}
/* Deal the shares and publish the group's key.
*
* This is a trusted dealer: for the length of one call, this machine holds
* enough to reconstruct the group's private key. Acceptable for testing, and
* where one party is trusted already. A distributed key generation produces
* the same shares without that moment ever existing, and this module does not
* provide one. */
static int deal_shares(const secp256k1_context *ctx, struct group *group) {
secp256k1_iceberg_share *share_ptr[N];
const secp256k1_iceberg_pubshare *pubshare_ptr[N];
unsigned char seed[32];
unsigned int k;
int ok;
step("Dealing shares (secp256k1_iceberg_shares_gen)");
if (!fill_random(seed, sizeof(seed))) {
return 0;
}
for (k = 0; k < N; k++) {
share_ptr[k] = &group->member[k].share;
}
ok = secp256k1_iceberg_shares_gen(ctx, share_ptr, N, T, seed);
secure_erase(seed, sizeof(seed));
if (!ok) {
return 0;
}
printf("ok\n");
/* A share is one seed per (t-1)-subset that leaves this participant out,
* so it is large and grows quickly with the group. Each participant keeps
* its own and nothing else. */
step("Storing them (secp256k1_iceberg_share_serialize)");
for (k = 0; k < N; k++) {
group->disk[k].share_len = sizeof(group->disk[k].share_bytes);
if (!secp256k1_iceberg_share_serialize(ctx, group->disk[k].share_bytes,
&group->disk[k].share_len, &group->member[k].share)) {
return 0;
}
/* Nothing answered yet, so the lowest possible label. Do this once, at
* dealing; doing it again later throws the protection away. */
memset(group->member[k].answered, 0, sizeof(group->member[k].answered));
memcpy(group->disk[k].answered_bytes, group->member[k].answered, 32);
}
printf("ok, %lu bytes each\n", (unsigned long)group->disk[0].share_len);
step("Group public key (secp256k1_iceberg_pubkey_agg)");
for (k = 0; k < N; k++) {
if (!secp256k1_iceberg_pubshare_gen(ctx, &group->pubshare[k], &group->member[k].share, NULL)) {
return 0;
}
pubshare_ptr[k] = &group->pubshare[k];
}
/* 2t-1 public shares are more than the key needs, since t of them already
* determine it, and the surplus is the point: they have to agree, so a
* participant that published a wrong one is caught now rather than at
* signing time. */
if (!secp256k1_iceberg_pubkey_agg(ctx, &group->pubkey, pubshare_ptr, MU, N, T)) {
return 0;
}
printf("ok\n");
return 1;
}
/* Aggregate the group's key with the cosigner's, and tweak the result.
*
* Nothing below this point knows that one of the two keys is a group. The
* tweak belongs to this outer session: musig_nonce_process sets its term aside
* and musig_partial_sig_agg adds it once, at the top. No Iceberg call is even
* told the tweak happened, which is what stops it being counted twice. */
static int aggregate_keys(const secp256k1_context *ctx, struct group *group, struct cosigner *cosigner, struct shared_key *shared) {
const secp256k1_pubkey *pubkeys[2];
secp256k1_pubkey output_pk;
unsigned char taptweak[32];
unsigned char seckey[32];
int ok;
step("Cosigner keypair (secp256k1_keypair_create)");
ok = fill_random(seckey, sizeof(seckey))
&& secp256k1_keypair_create(ctx, &cosigner->keypair, seckey)
&& secp256k1_keypair_pub(ctx, &cosigner->pubkey, &cosigner->keypair);
/* The keypair holds everything needed from here on, so the raw key does not
* outlive this call; secp256k1_keypair_sec brings it back when required. */
secure_erase(seckey, sizeof(seckey));
if (!ok) {
return 0;
}
printf("ok\n");
step("Aggregating (secp256k1_musig_pubkey_agg)");
pubkeys[0] = &group->pubkey;
pubkeys[1] = &cosigner->pubkey;
if (!secp256k1_musig_pubkey_agg(ctx, NULL, &shared->keyagg_cache, pubkeys, 2)) {
return 0;
}
printf("ok\n");
/* The cache records the hash of the key list, not the list, so nothing ties
* it to the group's key later. Run this once, here, rather than trusting a
* cache round two cannot check. */
step("Checking the cache (secp256k1_iceberg_keyagg_check)");
if (!secp256k1_iceberg_keyagg_check(ctx, &shared->keyagg_cache, pubkeys, 2,
&group->pubkey)) {
return 0;
}
printf("ok\n");
step("Tweaking (secp256k1_musig_pubkey_xonly_tweak_add)");
if (!fill_random(taptweak, sizeof(taptweak))
|| !secp256k1_musig_pubkey_xonly_tweak_add(ctx, &output_pk, &shared->keyagg_cache, taptweak)
|| !secp256k1_xonly_pubkey_from_pubkey(ctx, &shared->output, NULL, &output_pk)) {
return 0;
}
printf("ok\n");
return 1;
}
/* Round one, the group's half. Note the arguments: a share and a label. No
* message, no cosigner, nothing from anybody else. */
static int group_round_one(const secp256k1_context *ctx, struct group *group,
struct signing_session *session) {
unsigned int k;
/* The label is the most delicate value in the scheme, and it is the
* caller's to choose. Every participant's secret nonces are a function of
* (its seeds, this label), and the seeds never change, so whoever picks the
* label picks everyone's secrets.
*
* Here it is a fixed byte string, which is fine for an example and wrong
* for anything else. In Lightning it is the commitment number: unique,
* strictly increasing over the channel's life, and known before the
* transaction is assembled, which is what lets this round run at all,
* since the message does not exist yet. */
step("Choosing the session label");
memset(session->sid, 0x2c, sizeof(session->sid));
printf("ok, ");
print_hex(session->sid, 4);
step("Participant nonces (secp256k1_iceberg_nonce_gen)");
for (k = 0; k < MU; k++) {
if (!secp256k1_iceberg_nonce_gen(ctx, &session->contribution[k], &group->member[k].share, NULL, session->sid)) {
return 0;
}
session->contribution_ptr[k] = &session->contribution[k];
}
printf("ok, %d of the %d participants\n", MU, N);
/* Verification, not addition. The contributions are points on a degree
* t-1 polynomial in the exponent; this checks that they lie on one and
* then interpolates. It is most of what the group costs, and it is what
* stops a single participant biasing the group's nonce.
*
* NULL is the group's internal aggregate, which nothing takes back. */
step("Combining them (secp256k1_iceberg_nonce_agg)");
if (!secp256k1_iceberg_nonce_agg(ctx, &session->group_nonce, NULL,
session->contribution_ptr, MU, N, T, &group->pubkey)) {
return 0;
}
printf("ok\n");
return 1;
}
/* Round one, the cosigner's half. Ordinary MuSig2, and it does not know a group
* exists. Runs before, after or alongside the function above. */
static int cosigner_round_one(const secp256k1_context *ctx, struct cosigner *cosigner, struct shared_key *shared,
struct signing_session *session, const unsigned char *msg32) {
const secp256k1_musig_pubnonce *just_the_cosigner[1];
unsigned char secrand[32];
unsigned char seckey[32];
int ok;
step("Cosigner nonce (secp256k1_musig_nonce_gen)");
ok = fill_random(secrand, sizeof(secrand))
&& secp256k1_keypair_sec(ctx, seckey, &cosigner->keypair)
&& secp256k1_musig_nonce_gen(ctx, &cosigner->secnonce, &session->cosigner_nonce,
secrand, seckey, &cosigner->pubkey,
msg32, &shared->keyagg_cache, NULL);
secure_erase(secrand, sizeof(secrand));
secure_erase(seckey, sizeof(seckey));
if (!ok) {
return 0;
}
/* Round two needs the cosigners' aggregate on its own, separately from the
* one that includes the group. With one cosigner it is an aggregate of one,
* which is not a special case anywhere. */
just_the_cosigner[0] = &session->cosigner_nonce;
if (!secp256k1_musig_nonce_agg(ctx, &session->cosigner_aggnonce, just_the_cosigner, 1)) {
return 0;
}
printf("ok\n");
return 1;
}
/* Both halves have published. Combine them into the session everybody signs
* against, which is where the message finally enters. */
static int open_session(const secp256k1_context *ctx, struct shared_key *shared,
struct signing_session *session, const unsigned char *msg32) {
const secp256k1_musig_pubnonce *both[2];
secp256k1_musig_aggnonce full_aggnonce;
step("Session (secp256k1_musig_nonce_process)");
both[0] = &session->group_nonce;
both[1] = &session->cosigner_nonce;
if (!secp256k1_musig_nonce_agg(ctx, &full_aggnonce, both, 2)
|| !secp256k1_musig_nonce_process(ctx, &session->musig_session, &full_aggnonce,
msg32, &shared->keyagg_cache, NULL)) {
return 0;
}
printf("ok\n");
return 1;
}
/* Forget the secret nonces, then rebuild from disk.
*
* A FROST signer has to keep a secret nonce alive across this line, and losing
* it, restoring an old backup over it, or running two copies of the signer are
* each catastrophic. An Iceberg participant keeps no secret nonce at all: its
* nonces are a function of its share and the label, both of which it is given
* again.
*
* That property is narrower than "keeps no state".
* A participant still has to remember which labels it has already answered
* under, because two answers under one label are two equations in its three
* secrets and three are enough to recover its key share. That bookkeeping is
* not secret, and it is not optional; it is simply not this library's, since
* only the group as a whole can decide which message a label belongs to.
*
* So wipe the secrets and prove that much. Skip the parse below and the next
* call is handed a wiped share, whose magic fails an ARG_CHECK, so it aborts
* through the illegal callback rather than returning 0. */
static int reboot_participants(const secp256k1_context *ctx, struct group *group) {
unsigned int k;
step("Wiping every participant's secret material");
for (k = 0; k < N; k++) {
secure_erase(&group->member[k], sizeof(group->member[k]));
}
printf("ok\n");
step("Rebuilding (secp256k1_iceberg_share_parse)");
for (k = 0; k < N; k++) {
if (!secp256k1_iceberg_share_parse(ctx, &group->member[k].share,
group->disk[k].share_bytes,
group->disk[k].share_len)) {
return 0;
}
memcpy(group->member[k].answered, group->disk[k].answered_bytes, 32);
}
printf("ok\n");
/* Note what came back besides the share: `answered`. That record is not
* secret, which is why it sits on the same disk in the clear, and it is
* not optional, which is why it is restored here rather than left at zero.
* A participant that forgets it will answer twice. */
return 1;
}
/* One label, one answer: the half of the rule a participant can enforce alone.
*
* The library cannot do this for you. It holds nothing between calls, so "have I
* answered under this label before?" is a question only the caller's storage can
* answer. Here that storage is one 32-byte field per participant and the rule is
* that a label must be strictly greater than the last one signed under, which is
* free when the label is a counter. It refuses a repeat outright rather than
* asking whether the repeat was harmless: a label is one signing attempt, so an
* honest retry arrives under a new label and never needs the exception.
*
* The record reaches disk here, before the caller has a share to publish. A
* crash between signing and storing is the same as never having stored, and the
* next boot answers the label again.
*
* The other half of the rule, that no two participants answer one label on
* different messages, cannot be checked here or anywhere else inside a
* participant, because it is a fact about what other people were shown. See
* doc/iceberg.md. */
static int may_sign_under(struct participant *member, struct storage *disk, const unsigned char *sid32) {
if (memcmp(sid32, member->answered, 32) <= 0) {
return 0;
}
memcpy(member->answered, sid32, 32);
memcpy(disk->answered_bytes, member->answered, 32);
return 1;
}
/* The group's half of round two, signed by participants first, first+1 and
* first+2.
*
* Taking `first` is not generality for its own sake. The point of the scheme is
* that any t of them will do, so main runs this twice with different people and
* compares the two signatures.
*
* Any t of the seven, and not only the five who were in round one. The last
* section of main proves that by having a participant who sat round one out
* sign anyway. What each signer does need is the whole round-one set to check
* against, which is a different count from the number of signers. */
static int group_signs(const secp256k1_context *ctx, struct group *group,
struct shared_key *shared, struct signing_session *session,
unsigned int first, const unsigned char *msg32,
secp256k1_musig_partial_sig *group_partial_sig) {
secp256k1_iceberg_partial_sig sig_share[T];
const secp256k1_iceberg_partial_sig *sig_share_ptr[T];
unsigned int k;
/* Note what this takes: the group's own round-one contributions, not an
* aggregate of them, and not the cosigners', which arrive separately and
* already aggregated. Do not read that as an optimization waiting to happen.
* The nesting coefficient is a hash of the group's aggregate nonce, so a
* coordinator free to invent that aggregate has a coefficient it can vary at
* will, and three invented aggregates under one label give three equations in
* the same three unknowns, and the third is the key. So the aggregate is
* derived here from the contributions instead. */
step("Signature shares (secp256k1_iceberg_partial_sign)");
for (k = 0; k < T; k++) {
if (!may_sign_under(&group->member[first + k], &group->disk[first + k], session->sid)) {
printf("refused: participant %d has already answered under this label\n",
first + k + 1);
return 0;
}
/* The signer derives the aggregate from the contributions, then checks
* the polynomial they determine against the contribution it derives for
* itself, which ties the set to this label. It need not have been one of
* the contributors. */
if (!secp256k1_iceberg_partial_sign(ctx, &sig_share[k], &group->member[first + k].share, NULL,
session->sid, session->contribution_ptr, MU,
&group->pubkey, &shared->keyagg_cache,
msg32, &session->cosigner_aggnonce)) {
return 0;
}
sig_share_ptr[k] = &sig_share[k];
}
printf("ok, from participants %d, %d and %d\n", first + 1, first + 2, first + 3);
/* Optional, and the coordinator's to decide on. Skipping it costs nothing
* until a share is bad, at which point the final signature simply fails
* BIP-340 and says nothing about which of the three caused it. Checking
* costs about what producing a share costs, per share.
*
* That is with exactly t shares, which is what this collects. Hand
* secp256k1_iceberg_partial_sig_agg one more than it needs and it refuses a
* set that disagrees with itself, cheaply and without naming anybody, for
* the same reason pubkey_agg can above: past the threshold there is a spare
* point to check against.
*
* It answers "is this share bad", not "who is lying". A share that fails
* here may have been written by somebody other than the member it names,
* and the same 0 comes back if this machine has the wrong message. */
step("Checking them (secp256k1_iceberg_partial_sig_verify)");
for (k = 0; k < T; k++) {
if (!secp256k1_iceberg_partial_sig_verify(ctx, &sig_share[k], &group->pubshare[first + k],
session->contribution_ptr, MU, N, T,
&group->pubkey, &shared->keyagg_cache, msg32,
&session->cosigner_aggnonce)) {
printf("share from participant %d does not check out\n", first + k + 1);
return 0;
}
}
printf("ok, all %d\n", T);
step("Combining (secp256k1_iceberg_partial_sig_agg)");
if (!secp256k1_iceberg_partial_sig_agg(ctx, group_partial_sig, sig_share_ptr, T, N, T)) {
return 0;
}
printf("ok\n");
return 1;
}
/* The cosigner's half, which happens exactly once.
*
* secp256k1_musig_partial_sign consumes the secret nonce, zeroing it on
* the way out, precisely so that signing twice with it is not something a
* caller can do by accident. Which quorum the group fielded is not the
* cosigner's business and does not change its answer. */
static int cosigner_signs(const secp256k1_context *ctx, struct cosigner *cosigner,
struct shared_key *shared, struct signing_session *session,
secp256k1_musig_partial_sig *cosigner_partial_sig) {
step("Cosigner's share (secp256k1_musig_partial_sign)");
if (!secp256k1_musig_partial_sign(ctx, cosigner_partial_sig, &cosigner->secnonce,
&cosigner->keypair, &shared->keyagg_cache,
&session->musig_session)) {
return 0;
}
printf("ok\n");
return 1;
}
/* One group share plus one cosigner share makes an ordinary MuSig2 signature. */
static int combine(const secp256k1_context *ctx, struct signing_session *session,
const secp256k1_musig_partial_sig *group_partial_sig,
const secp256k1_musig_partial_sig *cosigner_partial_sig,
unsigned char *sig64) {
const secp256k1_musig_partial_sig *both[2];
step("Final signature (secp256k1_musig_partial_sig_agg)");
both[0] = group_partial_sig;
both[1] = cosigner_partial_sig;
if (!secp256k1_musig_partial_sig_agg(ctx, sig64, &session->musig_session, both, 2)) {
return 0;
}
printf("ok\n");
return 1;
}
/* One call that must be refused and one that must not, with the reason beside
* each.
*
* The refusal below is a load-bearing security check that looks like a bug from
* outside; the call after it is the case that looks refusable and must not
* be. */
static int refusals_are_refused(const secp256k1_context *ctx, struct group *group,
struct shared_key *shared, struct signing_session *session,
const unsigned char *msg32) {
secp256k1_iceberg_partial_sig sig_share;
step("Signing over contributions from another session");
{
/* The contributions below are a real sharing, internally consistent
* and passing the degree check, but they were produced under a
* different label. Accepting them would give whoever supplied them a
* coefficient in the signing equation that this participant cannot
* check, which is worth a signature share to an adversary and nothing
* to anybody else.
*
* The signer catches it by interpolating the contributions and
* evaluating the result at its own index: the polynomial does not pass
* through the contribution it derives locally for the label it is
* signing under. */
secp256k1_iceberg_pubnonce elsewhere[MU];
const secp256k1_iceberg_pubnonce *elsewhere_ptrs[MU];
unsigned char other_sid[32];
unsigned int k;
memcpy(other_sid, session->sid, sizeof(other_sid));
other_sid[0] ^= 1;
for (k = 0; k < MU; k++) {
if (!secp256k1_iceberg_nonce_gen(ctx, &elsewhere[k], &group->member[k].share,
NULL, other_sid)) {
return 0;
}
elsewhere_ptrs[k] = &elsewhere[k];
}
/* The set is a perfectly good sharing. What it is not is a sharing of
* *this* label, and only the signer can tell, because only the signer
* holds the share that says what its own contribution should have
* been. */
if (secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[0].share, NULL,
session->sid, elsewhere_ptrs, MU, &group->pubkey,
&shared->keyagg_cache, msg32,
&session->cosigner_aggnonce)) {
printf("FAILED: it signed, and it should not have\n");
return 0;
}
}
printf("refused, correctly\n");
/* The other half of that check is a thing it must NOT cost. */
step("Signing by a member who sat round one out");
{
/* Participants 6 and 7 were not among the 2t-1 who produced nonces.
* They can still sign, because a contribution is a function of the
* share and the label, so an absent member can derive what its
* contribution would have been and check the polynomial against it
* without ever having been one of the contributors.
*
* This is the property that lets a quorum change between the rounds,
* which is most of why the scheme works this way: keys sit in
* cold storage and members are routinely absent rather than hostile. */
if (!secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[N - 1].share, NULL,
session->sid, session->contribution_ptr, MU,
&group->pubkey, &shared->keyagg_cache, msg32,
&session->cosigner_aggnonce)) {
printf("FAILED: it refused, and it should not have\n");
return 0;
}
}
printf("signed, correctly\n");
return 1;
}
static void print_caveats(void) {
heading("WHAT THIS EXAMPLE IS NOT");
printf(" A trusted dealer deals the shares. Real deployments want a\n");
printf(" distributed key generation, which this module does not provide.\n");
printf("\n");
printf(" Everything is passed as structs in one process. The wire formats\n");
printf(" exist: 34 bytes for a public share, 67 for a nonce contribution,\n");
printf(" 33 for a signature share. Nothing here uses them, so the\n");
printf(" network is the part you still have to write.\n");
printf("\n");
printf(" Structs in one process also satisfy constraint 2 for free. Over a\n");
printf(" network the channel must be authenticated and a contribution taken\n");
printf(" only from the member its index names; without that, an adversary\n");
printf(" supplying t of the 2t-1 picks the group's nonce and every check\n");
printf(" here still passes.\n");
printf("\n");
printf(" Verifying a share tells you whether it satisfies the equation,\n");
printf(" not who is at fault: partial signatures are forgeable, so a share\n");
printf(" that fails may have been written by somebody else.\n");
printf("\n");
printf(" may_sign_under above is half the rule, and the easy half: it\n");
printf(" stops one participant answering twice. Nothing anywhere stops two\n");
printf(" participants answering one label on different messages. Two answers\n");
printf(" under one label are two equations in a participant's three\n");
printf(" secrets; three recover its key share.\n");
printf("\n");
printf(" The label is an argument, rather than something derived from the\n");
printf(" message, because round one has to run before the message exists.\n");
printf(" In Lightning the nonce is fixed a round-trip before the\n");
printf(" transaction is assembled, and the commitment number is what\n");
printf(" Lightning supplies instead.\n");
printf("\n");
printf(" So half of that discipline is yours, never answering twice under\n");
printf(" one label, and half is the group's: agree which message a label\n");
printf(" belongs to before anyone answers. The unforgeability proof, which\n");
printf(" is not yet peer-reviewed, assumes both. See doc/iceberg.md.\n");
}
int main(void) {
secp256k1_context *ctx;
struct group group;
struct cosigner cosigner;
struct shared_key shared;
struct signing_session session;
unsigned char msg[32] = "this_could_be_the_hash_of_a_msg";
secp256k1_musig_partial_sig group_partial_sig, cosigner_partial_sig;
unsigned char sig[64], sig_from_the_others[64];
unsigned int k;
int ok;
ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
printf("Iceberg: a %d-of-%d group signing as one MuSig2 participant.\n\n", T, N);
printf(" round one needs %d of the %d participants; round two needs %d\n", MU, N, MU);
printf(" online again, not necessarily the same ones, and combines %d\n", T);
printf(" signature shares. See doc/iceberg.md for why those differ.\n");
heading("Setup: once, by a dealer who is then not needed again");
ok = deal_shares(ctx, &group);
if (ok) {
heading("Key aggregation: the group is now just a public key");
ok = aggregate_keys(ctx, &group, &cosigner, &shared);
}
if (ok) {
/* The two halves are written in this order because something has to
* go first on the page. Swap the two calls and the example still
* passes, since neither needs anything the other produces, which is why
* two Iceberg groups can sign with each other. */
heading("Round one: needs 2t-1 participants, in no particular order");
ok = group_round_one(ctx, &group, &session)
&& cosigner_round_one(ctx, &cosigner, &shared, &session, msg)
&& open_session(ctx, &shared, &session, msg);
}
if (ok) {
heading("The gap: every participant forgets its secret nonces");
ok = reboot_participants(ctx, &group);
}
if (ok) {
heading("Round two: t shares, from any members, not just round one's");
ok = group_signs(ctx, &group, &shared, &session, 0, msg, &group_partial_sig)
&& cosigner_signs(ctx, &cosigner, &shared, &session, &cosigner_partial_sig)
&& combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig);
}
if (ok) {
step("Verifying (secp256k1_schnorrsig_verify)");
ok = secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &shared.output);
if (ok) {
printf("ok\n ");
print_hex(sig, sizeof(sig));
printf(" An ordinary BIP-340 signature. Nothing in it records a group.\n");
}
}
if (ok) {
heading("The same signature, from different people");
/* Participants 4, 5 and 6 this time, with no overlap with 1, 2 and 3, and
* 6 was not in round one either. Both of those matter. Disjoint,
* because nobody may answer twice under one label. The library holds
* nothing between calls and cannot detect that, so may_sign_under above
* is what refuses it. And 6 absent from round one, because a member that
* was away can still work out what its contribution would have been.
*
* Only the group signs again. The cosigner's share is reused, since it
* has one secret nonce and spending it twice would be nonce reuse. */
ok = group_signs(ctx, &group, &shared, &session, T, msg, &group_partial_sig)
&& combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig_from_the_others);
}
if (ok) {
step("Comparing the two signatures");
ok = memcmp(sig, sig_from_the_others, sizeof(sig)) == 0;
printf(ok ? "byte-identical\n" : "the two quorums disagreed\n");
}
if (ok) {
heading("What the module checks, and what it leaves to you");
ok = refusals_are_refused(ctx, &group, &shared, &session, msg);
}
if (!ok) {
printf("FAILED\n");
}
print_caveats();
/* Clear the secrets: a bug elsewhere that leaks memory, or an OS that swaps
* it to disk, should not find them lying around. The group's shares are the
* long-term secret here, and the cosigner's keypair and secret nonce are
* the rest. */
for (k = 0; k < N; k++) {
secure_erase(&group.member[k], sizeof(group.member[k]));
secure_erase(&group.disk[k], sizeof(group.disk[k]));
}
secure_erase(&cosigner, sizeof(cosigner));
secp256k1_context_destroy(ctx);
return ok ? EXIT_SUCCESS : EXIT_FAILURE;
}