iceberg: add the Iceberg threshold-MuSig module

Port the experimental Iceberg module from the benchmark-iceberg tree
(github.com/furszy/benchmark-iceberg, sources/secp256k1-kmp/native/
secp256k1) into this repo.

Iceberg is a threshold scheme that lets a group of parties stand in
for a single MuSig2 (BIP 327) participant: the group produces one
ordinary MuSig2 public nonce and one ordinary MuSig2 partial
signature, so cosigners cannot tell a group is involved and need no
changes. Nonces are derived from a caller-chosen per-session label
(sid32) rather than stored, so no signer holds a secret nonce between
rounds; labels are public but must never be reused. A quorum of 2t-1
members (of whom up to t-1 may be corrupt) is needed in each round,
so the threshold is at most half the group rounded up; combined with
the scheme's other constraints the smallest usable group is 2-of-4.
See doc/iceberg.md and the module header for the full usage notes.

Module layout (src/modules/iceberg/, layered bottom-up, each layer
may only use the ones above it -- that ordering is also the
constant-time story):
- scalar_poly.{h,_impl.h}: secret-carrying polynomial arithmetic,
  keeping secrets away from inversions (documented in the header).
- rss.{h,_impl.h}: replicated secret sharing evaluation.
- vpss.{h,_impl.h}: verifiable public shares; variable-time by
  design, sees only participant indices and published points.
- keygen_impl.h: distributed key generation producing one share per
  member.
- session_impl.h: nonce_gen/nonce_agg and partial_sign/
  partial_sig_agg producing plain MuSig2 objects.
- tests_impl.h: 28 tests including the shipped vectors.h vector
  suite and dealer known-answer tests.
- bench_impl.h: benchmark definitions (wired in a follow-up commit).

Public headers: include/secp256k1_iceberg.h (installed) and
include/secp256k1_iceberg_dealer.h (in-tree only: a trusted dealer is
not part of the shipped API, but tests, benchmarks and the example
need to deal shares).

Content adaptations relative to the source tree (the only changes to
the ported code): three secp256k1_musig_nonce_process call sites in
tests_impl.h gained a NULL adaptor argument, because this repo's
musig is the zkp variant whose public nonce_process takes an optional
adaptor point. All musig internals the module uses (ge_parse_ext,
ge_serialize_ext, keyaggcoef, aggnonce_load, pubnonce_save,
partial_sig_save, nonce_process_internal) are identical in both
trees, as are all core headers the module touches; nothing else
needed adaptation.

Build wiring mirrors the chilldkg module:
- configure.ac: --enable-module-iceberg (default no, experimental
  gate), hard dependency on the musig module with a configure error
  if musig is explicitly disabled (musig itself pulls in schnorrsig),
  AM_CONDITIONAL(ENABLE_MODULE_ICEBERG), summary line.
- Makefile.am: include src/modules/iceberg/Makefile.am.include under
  the conditional.
- src/secp256k1.c: guarded include of modules/iceberg/main_impl.h
  after the chilldkg block (musig is included earlier, so its
  internals are in scope).
- src/tests.c: module test registration via MAKE_TEST_MODULE(iceberg).
- CMakeLists.txt / src/CMakeLists.txt: SECP256K1_ENABLE_MODULE_ICEBERG
  option (OFF) with a dependency check on SECP256K1_ENABLE_MODULE_MUSIG
  (placed before the musig block so the force-enable takes effect),
  ENABLE_MODULE_ICEBERG=1 compile definition, public header export,
  summary line.

Verified: ./configure --enable-experimental --enable-module-iceberg
&& make check passes; ./tests --target=iceberg runs the full module
suite (28/28); CMake build + ctest pass; the musig dependency error
fires correctly in both build systems.
This commit is contained in:
Kgothatso Ngako
2026-08-31 12:24:48 +02:00
parent e9f171f197
commit e581abad00
22 changed files with 8557 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
include_HEADERS += include/secp256k1_iceberg.h
# In the tree so tests, benchmarks and the example can deal shares; not
# installed, because a trusted dealer is not part of what this library offers.
noinst_HEADERS += include/secp256k1_iceberg_dealer.h
noinst_HEADERS += src/modules/iceberg/main_impl.h
noinst_HEADERS += src/modules/iceberg/bench_impl.h
noinst_HEADERS += src/modules/iceberg/scalar_poly.h
noinst_HEADERS += src/modules/iceberg/scalar_poly_impl.h
noinst_HEADERS += src/modules/iceberg/rss.h
noinst_HEADERS += src/modules/iceberg/rss_impl.h
noinst_HEADERS += src/modules/iceberg/vpss.h
noinst_HEADERS += src/modules/iceberg/vpss_impl.h
noinst_HEADERS += src/modules/iceberg/keygen_impl.h
noinst_HEADERS += src/modules/iceberg/session_impl.h
noinst_HEADERS += src/modules/iceberg/tests_impl.h
noinst_HEADERS += src/modules/iceberg/vectors.h

View File

@@ -0,0 +1,221 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_BENCH_H
#define SECP256K1_MODULE_ICEBERG_BENCH_H
#include <string.h>
#include "../../../include/secp256k1_iceberg.h"
#include "../../../include/secp256k1_iceberg_dealer.h"
/* Timings for each step of a signing session.
*
* Two growth laws meet here, which is what the configurations are chosen to
* show rather than any of them being deployable; see doc/iceberg.md. Each
* participant holds C(n-1, t-1) seeds (2 at 2-of-3, 126 at 5-of-10) and
* hashes every one on every derivation, while the curve work grows only with the
* quorum 2t-1, since that is the width of the multiexponentiation in the degree
* check and everything else on the curve is a fixed number of multiplications.
* The two grow at different rates. */
typedef struct {
secp256k1_context *ctx;
unsigned int n, t, mu;
secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_iceberg_share_cache cache;
secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_iceberg_pubnonce nonces[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
const secp256k1_iceberg_pubnonce *nonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_iceberg_partial_sig psigs[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
const secp256k1_iceberg_partial_sig *psig_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_iceberg_aggnonce aggnonce;
secp256k1_pubkey group_pk, cosigner_pk;
secp256k1_keypair cosigner_keypair;
secp256k1_xonly_pubkey agg_xonly;
secp256k1_musig_keyagg_cache keyagg_cache;
secp256k1_musig_secnonce cosigner_secnonce;
secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce;
secp256k1_musig_aggnonce cosigner_aggnonce;
secp256k1_musig_partial_sig group_psig;
unsigned char seed[32], msg[32], sid[32];
/* One of the two nonce sharings, kept in the form the group layer wants it,
* so the degree check and the interpolation can be timed on their own. */
unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_ge commitments[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
} bench_iceberg_data;
static void bench_iceberg_setup(bench_iceberg_data *d) {
const secp256k1_pubkey *pubkeys[2];
const secp256k1_musig_pubnonce *one[1];
unsigned char seckey[32], secrand[32];
unsigned int k;
memset(d->seed, 0x2a, 32);
memset(d->msg, 0x5c, 32);
memset(seckey, 0x31, 32);
memset(secrand, 0x77, 32);
for (k = 1; k <= d->n; k++) {
d->share_ptrs[k - 1] = &d->shares[k - 1];
}
CHECK(secp256k1_iceberg_shares_gen(d->ctx, d->share_ptrs, d->n, d->t, d->seed));
CHECK(secp256k1_iceberg_share_cache_create(d->ctx, &d->cache, &d->shares[0]));
for (k = 1; k <= d->n; k++) {
CHECK(secp256k1_iceberg_pubshare_gen(d->ctx, &d->pubshares[k - 1], &d->shares[k - 1], NULL));
d->pubshare_ptrs[k - 1] = &d->pubshares[k - 1];
}
CHECK(secp256k1_iceberg_pubkey_agg(d->ctx, &d->group_pk, d->pubshare_ptrs, d->mu, d->n, d->t));
CHECK(secp256k1_keypair_create(d->ctx, &d->cosigner_keypair, seckey));
CHECK(secp256k1_keypair_pub(d->ctx, &d->cosigner_pk, &d->cosigner_keypair));
pubkeys[0] = &d->group_pk;
pubkeys[1] = &d->cosigner_pk;
CHECK(secp256k1_musig_pubkey_agg(d->ctx, &d->agg_xonly, &d->keyagg_cache, pubkeys, 2));
CHECK(secp256k1_musig_nonce_gen(d->ctx, &d->cosigner_secnonce, &d->cosigner_pubnonce,
secrand, seckey, &d->cosigner_pk, d->msg, &d->keyagg_cache, NULL));
one[0] = &d->cosigner_pubnonce;
CHECK(secp256k1_musig_nonce_agg(d->ctx, &d->cosigner_aggnonce, one, 1));
memset(d->sid, 0x2a, sizeof(d->sid));
for (k = 1; k <= d->n; k++) {
CHECK(secp256k1_iceberg_nonce_gen(d->ctx, &d->nonces[k - 1], &d->shares[k - 1], NULL, d->sid));
d->nonce_ptrs[k - 1] = &d->nonces[k - 1];
}
CHECK(secp256k1_iceberg_nonce_agg(d->ctx, &d->group_pubnonce, &d->aggnonce,
d->nonce_ptrs, d->mu, d->n, d->t, &d->group_pk));
for (k = 1; k <= d->mu; k++) {
secp256k1_ge pts[2];
unsigned int who;
CHECK(secp256k1_iceberg_pubnonce_load(d->ctx, &who, pts, &d->nonces[k - 1]));
d->idx[k - 1] = (unsigned char)who;
d->commitments[k - 1] = pts[0];
}
for (k = 1; k <= d->t; k++) {
/* A cache is bound to one participant, so pass NULL and let each of
* these derive its own. */
CHECK(secp256k1_iceberg_partial_sign(d->ctx, &d->psigs[k - 1], &d->shares[k - 1], NULL, d->sid, d->nonce_ptrs, d->mu, &d->group_pk, &d->keyagg_cache, d->msg, &d->cosigner_aggnonce));
d->psig_ptrs[k - 1] = &d->psigs[k - 1];
}
}
static void bench_iceberg_shares_gen(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_shares_gen(d->ctx, d->share_ptrs, d->n, d->t, d->seed));
}
}
static void bench_iceberg_cache_create(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_share_cache_create(d->ctx, &d->cache, &d->shares[0]));
}
}
static void bench_iceberg_pubshare_gen(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_pubshare_gen(d->ctx, &d->pubshares[0], &d->shares[0], &d->cache));
}
}
static void bench_iceberg_pubkey_agg(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_pubkey_agg(d->ctx, &d->group_pk, d->pubshare_ptrs, d->mu, d->n, d->t));
}
}
static void bench_iceberg_nonce_gen(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_nonce_gen(d->ctx, &d->nonces[0], &d->shares[0], &d->cache, d->sid));
}
}
static void bench_iceberg_nonce_agg(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_nonce_agg(d->ctx, &d->group_pubnonce, &d->aggnonce,
d->nonce_ptrs, d->mu, d->n, d->t, &d->group_pk));
}
}
static void bench_iceberg_partial_sign(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_partial_sign(d->ctx, &d->psigs[0], &d->shares[0], &d->cache, d->sid, d->nonce_ptrs, d->mu, &d->group_pk, &d->keyagg_cache, d->msg, &d->cosigner_aggnonce));
}
}
/* Optional, and priced per share rather than per signature: a caller that wants
* to know which share is bad runs this t times before aggregating once. */
static void bench_iceberg_partial_sig_verify(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_partial_sig_verify(d->ctx, &d->psigs[0], &d->pubshares[0], d->nonce_ptrs, d->mu, d->n, d->t, &d->group_pk, &d->keyagg_cache, d->msg, &d->cosigner_aggnonce));
}
}
/* The degree check and the interpolation, timed on their own. Both run once per
* nonce sharing inside nonce_agg, again inside partial_sign and inside
* partial_sig_verify, and once more in pubkey_agg at setup.
* Nothing in the API reaches these directly; they are here to take the rows
* above apart. */
static void bench_iceberg_degree_check(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_vpss_verify_var(d->ctx, d->idx, d->commitments, d->mu, d->t));
}
}
static void bench_iceberg_interpolate(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
secp256k1_gej r;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_vpss_combine_var(d->ctx, &r, d->idx, d->commitments, d->mu));
}
}
/* And inside the degree check, the m x m basis, which depends on nothing but
* the participant indices. All four checks in a signing session build the same
* one. */
static void bench_iceberg_lagrange_basis(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
secp256k1_scalar basis[SECP256K1_ICEBERG_MAX_PARTICIPANTS * SECP256K1_ICEBERG_MAX_PARTICIPANTS];
int i;
for (i = 0; i < iters; i++) {
secp256k1_scalarpoly_lagrange_basis_var(basis, d->idx, d->mu);
}
}
static void bench_iceberg_partial_sig_agg(void *arg, int iters) {
bench_iceberg_data *d = (bench_iceberg_data *)arg;
int i;
for (i = 0; i < iters; i++) {
CHECK(secp256k1_iceberg_partial_sig_agg(d->ctx, &d->group_psig, d->psig_ptrs,
d->t, d->n, d->t));
}
}
#endif /* SECP256K1_MODULE_ICEBERG_BENCH_H */

View File

@@ -0,0 +1,422 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_KEYGEN_IMPL_H
#define SECP256K1_MODULE_ICEBERG_KEYGEN_IMPL_H
#include <string.h>
#include "../../../include/secp256k1_iceberg.h"
#include "../../../include/secp256k1_iceberg_dealer.h"
#include "rss_impl.h"
#include "scalar_poly_impl.h"
#include "vpss_impl.h"
#include "../../group.h"
#include "../../hash.h"
#include "../../scalar.h"
#include "../../util.h"
/* The label under which the signing key itself is derived. Everything else the
* group produces is keyed on a session label; the key is keyed on this fixed
* string, so it is the one sharing that never changes. Fourteen bytes, matching
* the reference implementation; shares would not be portable otherwise. */
static const unsigned char secp256k1_iceberg_keygen_label[14] = {
'I', 'c', 'e', 'b', 'e', 'r', 'g', '/', 'k', 'e', 'y', 'g', 'e', 'n'
};
static const unsigned char secp256k1_iceberg_share_magic[4] = { 0x1c, 0xeb, 0x27, 0x5a };
static const unsigned char secp256k1_iceberg_cache_magic[4] = { 0x1c, 0xeb, 0xc4, 0x03 };
static const unsigned char secp256k1_iceberg_pubshare_magic[4] = { 0x1c, 0xeb, 0x9d, 0xf1 };
/* share: magic(4) | n(1) t(1) k(1) pad(1) | seeds[MAX_SEEDS][32]
* cache: magic(4) | n(1) t(1) k(1) pad(1) | weights[MAX_SEEDS][32]
* pubshare: magic(4) | index(1) | ge_to_bytes_ext(64)
*
* Both share and cache are sized for the worst configuration, not the one in
* use, so a 2-of-3 group carries mostly padding. That is the price of a
* fixed-size opaque type in a library that never allocates; the serialized
* forms are exact. */
static void secp256k1_iceberg_share_save(secp256k1_iceberg_share *share, unsigned int n, unsigned int t, unsigned int k) {
/* Zero the whole object, not just the header. Only C(n-1, t-1) of the seed
* slots are ever written, and leaving the rest as whatever was on the stack
* would make two shares holding identical secrets compare unequal, and
* would put uninitialized memory into anything that copies the struct. */
memset(share->data, 0, sizeof(share->data));
memcpy(share->data, secp256k1_iceberg_share_magic, 4);
share->data[4] = (unsigned char)n;
share->data[5] = (unsigned char)t;
share->data[6] = (unsigned char)k;
share->data[7] = 0;
}
/* Unpacks the header and hands back a pointer to the seeds. Returns the number
* of seeds, or 0 if the object is not a well-formed share. */
static size_t secp256k1_iceberg_share_load(const secp256k1_context *ctx, unsigned int *n, unsigned int *t, unsigned int *k, const unsigned char **seeds, const secp256k1_iceberg_share *share) {
ARG_CHECK(secp256k1_memcmp_var(share->data, secp256k1_iceberg_share_magic, 4) == 0);
*n = share->data[4];
*t = share->data[5];
*k = share->data[6];
ARG_CHECK(*n >= 1 && *n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
ARG_CHECK(*t >= 1 && *t <= (*n + 1) / 2);
ARG_CHECK(*k >= 1 && *k <= *n);
*seeds = &share->data[8];
return secp256k1_rss_binom(*n - 1, *t - 1);
}
/* TODO: this call is optional and every caller of it can pass NULL instead, so
* the saving has to justify an extra type in the API. What it avoids per call
* is C(n, t-1) subset unrankings and count*(2t+2) - 3 scalar multiplications,
* about 1500 at 5-of-10. bench_iceberg times share_cache_create and times the
* three consumers without a cache; it does not time them with one, so the
* saving is not yet a row you can read off. */
int secp256k1_iceberg_share_cache_create(const secp256k1_context *ctx, secp256k1_iceberg_share_cache *cache, const secp256k1_iceberg_share *share) {
secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS];
const unsigned char *seeds;
unsigned int n, t, k;
size_t count, i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(cache != NULL);
memset(cache, 0, sizeof(*cache));
ARG_CHECK(share != NULL);
count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share);
if (count == 0) {
return 0;
}
if (secp256k1_rss_lagrange_weights_var(weights, n, t, k) != count) {
return 0;
}
memcpy(cache->data, secp256k1_iceberg_cache_magic, 4);
cache->data[4] = (unsigned char)n;
cache->data[5] = (unsigned char)t;
cache->data[6] = (unsigned char)k;
cache->data[7] = 0;
for (i = 0; i < count; i++) {
secp256k1_scalar_get_b32(&cache->data[8 + 32 * i], &weights[i]);
}
return 1;
}
/* Loads weights for this share, deriving them if the caller did not supply a
* cache. */
static int secp256k1_iceberg_weights_for(const secp256k1_context *ctx, secp256k1_scalar *weights, size_t count, unsigned int n, unsigned int t, unsigned int k, const secp256k1_iceberg_share_cache *cache) {
size_t i;
if (cache == NULL) {
return secp256k1_rss_lagrange_weights_var(weights, n, t, k) == count;
}
ARG_CHECK(secp256k1_memcmp_var(cache->data, secp256k1_iceberg_cache_magic, 4) == 0);
/* A cache built for a different participant would silently produce a wrong
* share, so the pairing is checked and not assumed. */
ARG_CHECK(cache->data[4] == n && cache->data[5] == t && cache->data[6] == k);
for (i = 0; i < count; i++) {
secp256k1_scalar_set_b32(&weights[i], &cache->data[8 + 32 * i], NULL);
}
return 1;
}
/* Initializes SHA256 with fixed midstate. This midstate was computed by applying
* SHA256 to SHA256("Iceberg/dealer")||SHA256("Iceberg/dealer"). */
static void secp256k1_iceberg_dealer_sha256_tagged(secp256k1_sha256 *sha) {
static const uint32_t midstate[8] = {
0xb40815eaul, 0x9e117bfaul, 0x4a71724ful, 0x1f71a00eul,
0x19cf3ed1ul, 0xd5ff1efcul, 0xeb8b1dd5ul, 0x024d39e8ul
};
secp256k1_sha256_initialize_midstate(sha, 64, midstate);
}
/* The seed named after one subset: H_"Iceberg/dealer"(root || n || t || rank).
*
* It depends on the rank and not on who receives it, which is the whole of
* replicated sharing: every participant outside the subset is handed this same
* value. Deriving all of them from one root keeps dealing reproducible, which is
* what makes test vectors possible, and it also means the root is as sensitive
* as the group key and must be destroyed afterwards. */
static void secp256k1_iceberg_dealer_seed(const secp256k1_context *ctx, unsigned char *seed, const unsigned char *root32, unsigned int n, unsigned int t, uint32_t rank) {
secp256k1_sha256 sha;
unsigned char header[6];
header[0] = (unsigned char)n;
header[1] = (unsigned char)t;
header[2] = (unsigned char)(rank >> 24);
header[3] = (unsigned char)(rank >> 16);
header[4] = (unsigned char)(rank >> 8);
header[5] = (unsigned char)rank;
secp256k1_iceberg_dealer_sha256_tagged(&sha);
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, root32, 32);
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, header, sizeof(header));
secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &sha, seed);
secp256k1_sha256_clear(&sha);
}
int secp256k1_iceberg_shares_gen(const secp256k1_context *ctx, secp256k1_iceberg_share * const *shares, unsigned int n, unsigned int t, const unsigned char *seed32) {
unsigned char seed[32];
uint32_t total, rank;
unsigned int k;
VERIFY_CHECK(ctx != NULL);
/* The out-params cannot be zeroed before n is validated, since n is what
* says how many of them there are. A caller that ignores the return value
* keeps whatever it passed in. */
ARG_CHECK(shares != NULL);
ARG_CHECK(seed32 != NULL);
ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
/* The quorum needed to verify a sharing is 2t-1 and cannot exceed the
* group, which is what makes Iceberg a minority-threshold scheme. Rejecting
* here means callers meet the limitation at setup, not at signing.
*
* Written as t <= (n+1)/2 rather than 2t-1 <= n, which is the same
* predicate over the integers but not over unsigned int: t is the caller's
* to choose, and doubling it first lets a t near UINT_MAX wrap back inside
* the bound. The dealer would then find C(n, t-1) == 0, write no seeds at
* all, and return 1 on a group whose key is a public constant. */
ARG_CHECK(t >= 1 && t <= (n + 1) / 2);
for (k = 1; k <= n; k++) {
ARG_CHECK(shares[k - 1] != NULL);
}
/* One participant at a time. Its share is exactly the seeds whose subsets
* leave it out, appended in rank order.
*
* That ordering is the share layout, and it is not private to this function:
* secp256k1_rss_lagrange_weights_var walks the same subsets in the same order
* to produce one weight per seed, so seed i and weight i belong to each
* other. Dealing per participant costs a repeated derivation, since a
* subset's seed goes to every participant outside it, but the dealer runs
* once per group. */
total = secp256k1_rss_binom(n, t - 1);
for (k = 1; k <= n; k++) {
size_t held = 0;
secp256k1_iceberg_share_save(shares[k - 1], n, t, k);
for (rank = 0; rank < total; rank++) {
if (secp256k1_rss_subset_unrank(n, t - 1, rank) & (secp256k1_rss_subset)(1u << k)) {
continue;
}
secp256k1_iceberg_dealer_seed(ctx, seed, seed32, n, t, rank);
memcpy(&shares[k - 1]->data[8 + 32 * held], seed, 32);
held++;
}
}
secp256k1_memclear_explicit(seed, sizeof(seed));
return 1;
}
int secp256k1_iceberg_share_serialize(const secp256k1_context *ctx, unsigned char *out, size_t *outlen, const secp256k1_iceberg_share *share) {
const unsigned char *seeds;
unsigned int n, t, k;
size_t count, needed;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(outlen != NULL);
ARG_CHECK(out != NULL);
ARG_CHECK(share != NULL);
count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share);
if (count == 0) {
/* A malformed share has no length to report, and the caller is told to
* size a second call from *outlen. Leaving the old value there would
* send it round the same loop forever. */
*outlen = 0;
return 0;
}
needed = 4 + 32 * count;
if (*outlen < needed) {
*outlen = needed;
return 0;
}
out[0] = 1; /* format version */
out[1] = (unsigned char)n;
out[2] = (unsigned char)t;
out[3] = (unsigned char)k;
memcpy(&out[4], seeds, 32 * count);
*outlen = needed;
return 1;
}
int secp256k1_iceberg_share_parse(const secp256k1_context *ctx, secp256k1_iceberg_share *share, const unsigned char *in, size_t inlen) {
unsigned int n, t, k;
size_t count;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(share != NULL);
memset(share, 0, sizeof(*share));
ARG_CHECK(in != NULL);
if (inlen < 4 || in[0] != 1) {
return 0;
}
n = in[1];
t = in[2];
k = in[3];
if (n < 1 || n > SECP256K1_ICEBERG_MAX_PARTICIPANTS) {
return 0;
}
if (t < 1 || t > (n + 1) / 2 || k < 1 || k > n) {
return 0;
}
count = secp256k1_rss_binom(n - 1, t - 1);
if (inlen != 4 + 32 * count) {
return 0;
}
secp256k1_iceberg_share_save(share, n, t, k);
memcpy(&share->data[8], &in[4], 32 * count);
return 1;
}
static void secp256k1_iceberg_pubshare_save(secp256k1_iceberg_pubshare *pubshare, unsigned int k, const secp256k1_ge *ge) {
memcpy(pubshare->data, secp256k1_iceberg_pubshare_magic, 4);
pubshare->data[4] = (unsigned char)k;
secp256k1_ge_to_bytes_ext(&pubshare->data[5], ge);
}
static int secp256k1_iceberg_pubshare_load(const secp256k1_context *ctx, unsigned int *k, secp256k1_ge *ge, const secp256k1_iceberg_pubshare *pubshare) {
ARG_CHECK(secp256k1_memcmp_var(pubshare->data, secp256k1_iceberg_pubshare_magic, 4) == 0);
*k = pubshare->data[4];
ARG_CHECK(*k >= 1 && *k <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
secp256k1_ge_from_bytes_ext(ge, &pubshare->data[5]);
return 1;
}
int secp256k1_iceberg_pubshare_gen(const secp256k1_context *ctx, secp256k1_iceberg_pubshare *pubshare, const secp256k1_iceberg_share *share, const secp256k1_iceberg_share_cache *cache) {
secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS];
secp256k1_scalar d;
secp256k1_gej dj;
secp256k1_ge point;
const unsigned char *seeds;
unsigned int n, t, k;
size_t count;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(pubshare != NULL);
memset(pubshare, 0, sizeof(*pubshare));
ARG_CHECK(share != NULL);
ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share);
if (count == 0 || !secp256k1_iceberg_weights_for(ctx, weights, count, n, t, k, cache)) {
return 0;
}
secp256k1_rss_eval(secp256k1_get_hash_context(ctx), &d, seeds, weights, count,
secp256k1_iceberg_keygen_label, sizeof(secp256k1_iceberg_keygen_label));
secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &dj, &d);
secp256k1_ge_set_gej(&point, &dj);
/* The commitment is about to be published, so it stops being secret here. */
secp256k1_declassify(ctx, &point, sizeof(point));
secp256k1_iceberg_pubshare_save(pubshare, k, &point);
secp256k1_scalar_clear(&d);
return 1;
}
int secp256k1_iceberg_pubshare_serialize(const secp256k1_context *ctx, unsigned char *out34, const secp256k1_iceberg_pubshare *pubshare) {
secp256k1_ge point;
unsigned int k;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(out34 != NULL);
memset(out34, 0, 34);
ARG_CHECK(pubshare != NULL);
if (!secp256k1_iceberg_pubshare_load(ctx, &k, &point, pubshare)) {
return 0;
}
out34[0] = (unsigned char)k;
secp256k1_musig_ge_serialize_ext(&out34[1], &point);
return 1;
}
int secp256k1_iceberg_pubshare_parse(const secp256k1_context *ctx, secp256k1_iceberg_pubshare *pubshare, const unsigned char *in34) {
secp256k1_ge point;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(pubshare != NULL);
memset(pubshare, 0, sizeof(*pubshare));
ARG_CHECK(in34 != NULL);
if (in34[0] < 1 || in34[0] > SECP256K1_ICEBERG_MAX_PARTICIPANTS) {
return 0;
}
if (!secp256k1_musig_ge_parse_ext(&point, &in34[1])) {
return 0;
}
secp256k1_iceberg_pubshare_save(pubshare, in34[0], &point);
return 1;
}
int secp256k1_iceberg_pubkey_agg(const secp256k1_context *ctx, secp256k1_pubkey *group_pk, const secp256k1_iceberg_pubshare * const *pubshares, size_t n_pubshares, unsigned int n, unsigned int t) {
unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_ge points[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_gej combined;
secp256k1_ge result;
size_t i, j;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(group_pk != NULL);
memset(group_pk, 0, sizeof(*group_pk));
ARG_CHECK(pubshares != NULL);
ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
ARG_CHECK(t >= 1 && t <= (n + 1) / 2);
/* Below 2t-1 the degree check proves nothing: too few honest points remain
* to pin the true polynomial, and a coalition could present a consistent
* sharing of a value it chose. Safe to double t here: the bound above has
* already put it under (n+1)/2. How many members published is a fact about
* the group rather than a caller bug, so it returns; a null entry in the
* sweep below is a caller bug and gets the illegal callback.
*
* More shares than the group has is a repeated index by the pigeonhole, so
* the loop below would refuse it anyway. The bound is here because idx and
* points are sized for the largest group. */
if (n_pubshares < 2 * (size_t)t - 1 || n_pubshares > n) {
return 0;
}
for (i = 0; i < n_pubshares; i++) {
ARG_CHECK(pubshares[i] != NULL);
}
for (i = 0; i < n_pubshares; i++) {
unsigned int k;
if (!secp256k1_iceberg_pubshare_load(ctx, &k, &points[i], pubshares[i])) {
return 0;
}
/* An index above n names no member of this group, so nothing could have
* authenticated what it carries. */
if (k > n) {
return 0;
}
idx[i] = (unsigned char)k;
/* Duplicate indices would make the interpolation singular, and are how
* a caller most easily passes fewer distinct participants than it
* believes it has. */
for (j = 0; j < i; j++) {
if (idx[j] == idx[i]) {
return 0;
}
}
}
if (!secp256k1_vpss_verify_var(ctx, idx, points, n_pubshares, t)) {
return 0;
}
if (!secp256k1_vpss_combine_var(ctx, &combined, idx, points, n_pubshares)) {
return 0;
}
if (secp256k1_gej_is_infinity(&combined)) {
return 0;
}
secp256k1_ge_set_gej(&result, &combined);
secp256k1_pubkey_save(group_pk, &result);
return 1;
}
#endif /* SECP256K1_MODULE_ICEBERG_KEYGEN_IMPL_H */

View File

@@ -0,0 +1,22 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_MAIN_H
#define SECP256K1_MODULE_ICEBERG_MAIN_H
/* Layers, bottom up. Each one may use those above it in this list and nothing
* below. That ordering is also the constant-time story: vpss sees only
* participant indices and published points and is variable time throughout,
* while scalar_poly has secret values passed through it and keeps them away
* from its inversions, as the note at the top of scalar_poly.h sets out. Seed
* material reaches rss_eval and the scalars keygen and session derive from it,
* which is where the clearing discipline lives. */
#include "scalar_poly_impl.h"
#include "rss_impl.h"
#include "vpss_impl.h"
#include "keygen_impl.h"
#include "session_impl.h"
#endif

141
src/modules/iceberg/rss.h Normal file
View File

@@ -0,0 +1,141 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_RSS_H
#define SECP256K1_MODULE_ICEBERG_RSS_H
#include <stdint.h>
#include "../../hash.h"
#include "../../scalar.h"
#include "../../../include/secp256k1_iceberg.h"
#include "scalar_poly.h"
/* Replicated secret sharing, and the pseudorandom sharing built on top of it.
*
* The setup gives every (t-1)-subset of participants its own random seed, held
* by everyone outside that subset. A coalition of t-1 participants therefore
* holds every seed but one, the one named after the coalition itself, and
* that single unknown scalar is the scheme's entire security margin.
*
* From those seeds, any label w yields a fresh Shamir sharing with no
* communication at all:
*
* f_w(x) = sum over subsets a of H(phi_a, w) * L_a(x)
*
* L_a is one at zero and zero on every member of a, so f_w(0) is just the sum
* of the hashes, and participant k can evaluate f_w(k) alone: the terms it
* lacks the seeds for are exactly the terms L_a(k) sends to zero. That is what
* makes deterministic, stateless nonces possible.
*
* Of the three lower layers this is the only one that handles a secret:
* scalar_poly only has values passed through it and vpss sees none at all.
* Above it, keygen deals the seeds and session derives nonces from them, so the
* clearing discipline is theirs too. Seeds and the values derived from them are
* secret; participant indices, subset structure and every Lagrange weight are
* public, so work driven by those may be variable time. */
/* Derived bounds at ten participants. A quorum of 2t-1 cannot exceed the group,
* so t is at most (n+1)/2, which is 5; and the number of size-(t-1) subsets is
* largest at that maximum t, which is C(10, 4). C89 cannot evaluate a binomial
* at preprocessing time, so both are written out; the tests recompute them with
* secp256k1_rss_binom. */
#define SECP256K1_ICEBERG_MAX_T 5
#define SECP256K1_ICEBERG_MAX_SUBSETS 210
/* These three constants are the participant bound worked out by hand, so the
* compiler cannot see that they belong together. Raising the bound without
* recomputing them sizes the opaque types for one group and deals another, which
* overruns a share at run time instead of failing to build, so raising it is
* refused below. Lowering it is allowed: the suite checks all three against
* every configuration the new bound can express (tests_impl.h,
* run_iceberg_binom_test) and says which one is wrong. Drawing that distinction
* at preprocessing time is not possible, since C89 cannot evaluate a binomial. */
#if SECP256K1_ICEBERG_MAX_PARTICIPANTS > 10
#error "MAX_T and MAX_SUBSETS here, and MAX_SEEDS in secp256k1_iceberg.h, are written out for ten participants. Recompute all three before raising the bound."
#endif
/* Lowering the bound is worth doing for a build that will only ever run small
* groups: at five participants a share is 200 bytes rather than 4040. Lower
* SECP256K1_ICEBERG_MAX_SEEDS to C(n-1, (n+1)/2 - 1) along with it, and MAX_T
* and MAX_SUBSETS above to match.
*
* Ten reaches every configuration the scheme can express up to 4-of-10, which is
* also the largest that clears the n >= 3t-2 bound a deployment needs.
*
* The same growth sizes the module's stack frames, which at ten participants
* come to nearly twice the largest frame anywhere in the musig module. The
* figures move with the compiler, so measure rather than assume:
*
* gcc -fstack-usage -O2 -c src/secp256k1.c -I. -Isrc -Iinclude $(module defines)
* sort -t$'\t' -k2 -rn secp256k1.su | head
*
* TODO: sizing every share for the compile-time maximum is what makes the bound
* an ABI decision. A secp256k1_iceberg_share_size(n, t) with caller-provided
* storage, the shape secp256k1_preallocated.h already uses, would let a group
* pay for the configuration it runs and would end that. */
/* The number of summands in a participant's evaluation is the number of seeds
* it holds. */
#define SECP256K1_ICEBERG_MAX_SUMMANDS SECP256K1_ICEBERG_MAX_SEEDS
/* The weights for a whole share are inverted in one batch, so the batch bound
* has to cover a participant's seed count. */
#if SECP256K1_ICEBERG_MAX_SUMMANDS > SECP256K1_SCALARPOLY_MAX_BATCH
#error "SECP256K1_SCALARPOLY_MAX_BATCH is too small for the per-participant seed count"
#endif
/* A quorum interpolates through one point per member, so the point bound has to
* cover the group. */
#if SECP256K1_ICEBERG_MAX_PARTICIPANTS > SECP256K1_SCALARPOLY_MAX_POINTS
#error "SECP256K1_SCALARPOLY_MAX_POINTS is too small for the participant bound"
#endif
/* A subset of participants as a bitmask, with bit j set when participant j
* belongs. Participants are numbered from one, so bit zero is always clear. */
typedef uint16_t secp256k1_rss_subset;
/* Binomial coefficient, for arguments within the compile-time bound. */
static uint32_t secp256k1_rss_binom(unsigned int n, unsigned int size);
/* Position of a subset in lexicographic order over all size-`size` subsets of
* {1..n}, and its inverse. At n = 5, size = 2 the order is
*
* {1,2} {1,3} {1,4} {1,5} {2,3} {2,4} {2,5} {3,4} {3,5} {4,5}
* 0 1 2 3 4 5 6 7 8 9
*
* so {2,3} ranks 4. Neither function enumerates that list: ranking sums the
* binomials counting the subsets that come before, and unranking walks the same
* sum backwards.
*
* The ordering fixes the layout of a serialized share and which Lagrange weight
* pairs with which seed, so it is consensus-critical between implementations.
* The example above is written out because the reference's own docstring gives
* one that is off by one. `rank` here is a position in a list, unrelated to the
* rank of a matrix, which is the other thing the word means around this scheme. */
static uint32_t secp256k1_rss_subset_rank(unsigned int n, unsigned int size, secp256k1_rss_subset subset);
static secp256k1_rss_subset secp256k1_rss_subset_unrank(unsigned int n, unsigned int size, uint32_t rank);
/* r <- H(seed32 || w) as a scalar, using the "VPSS/prf" tag. Secret in, secret
* out. The reduction into the scalar field is unconditional, matching the
* reference implementation. */
static void secp256k1_rss_prf(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seed32, const unsigned char *w, size_t wlen);
/* weights[i] <- L_a(k) for the i'th subset a that participant k is outside of,
* in ascending rank order: the scalar its seed for a gets multiplied by, using
* the L_a defined at the top of this file. Public data throughout, so this is
* variable time and its result can be cached. Writes C(n-1, t-1) entries and
* returns that count. */
static size_t secp256k1_rss_lagrange_weights_var(secp256k1_scalar *weights, unsigned int n, unsigned int t, unsigned int k);
/* r <- f_w(k), the participant's share of the sharing labeled w.
*
* `seeds` is num 32-byte seeds and `weights` the matching output of
* secp256k1_rss_lagrange_weights_var, in the same order. Constant time in the
* seed values; the loop bound and access pattern depend only on public
* structure. */
static void secp256k1_rss_eval(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seeds, const secp256k1_scalar *weights, size_t num, const unsigned char *w, size_t wlen);
#endif /* SECP256K1_MODULE_ICEBERG_RSS_H */

View File

@@ -0,0 +1,181 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_RSS_IMPL_H
#define SECP256K1_MODULE_ICEBERG_RSS_IMPL_H
#include "rss.h"
#include "scalar_poly_impl.h"
#include "../../hash.h"
#include "../../scalar.h"
#include "../../util.h"
/* C(n, k), zero outside the participant bound and above the diagonal. Computed
* rather than tabulated so it cannot disagree with MAX_PARTICIPANTS: a table
* sized for one bound and written out for another overruns below it and
* zero-fills above it, and a C(n, t-1) of zero makes the dealer write no seeds
* and report success. Both arguments count participants, never secret. */
static uint32_t secp256k1_rss_binom(unsigned int n, unsigned int size) {
uint32_t result = 1;
unsigned int i;
if (n > SECP256K1_ICEBERG_MAX_PARTICIPANTS || size > n) {
return 0;
}
if (size > n - size) {
size = n - size;
}
for (i = 0; i < size; i++) {
/* C(n, i+1) = C(n, i) * (n-i) / (i+1). The division is exact at every
* step because the running product is itself a binomial coefficient, and
* halving size above caps the intermediate at C(10, 4) * 6 = 1260 at
* the participant bound. */
result = result * (n - i) / (i + 1);
}
return result;
}
/* Lexicographic rank, walking the participants in order. Every participant we
* pass over without selecting skips the whole block of subsets that would have
* chosen it next. */
static uint32_t secp256k1_rss_subset_rank(unsigned int n, unsigned int size, secp256k1_rss_subset subset) {
uint32_t rank = 0;
unsigned int remaining = size;
unsigned int j;
for (j = 1; j <= n && remaining > 0; j++) {
if (subset & (secp256k1_rss_subset)(1u << j)) {
remaining--;
} else {
rank += secp256k1_rss_binom(n - j, remaining - 1);
}
}
return rank;
}
static secp256k1_rss_subset secp256k1_rss_subset_unrank(unsigned int n, unsigned int size, uint32_t rank) {
secp256k1_rss_subset subset = 0;
unsigned int remaining = size;
unsigned int j = 1;
while (remaining > 0) {
uint32_t block;
VERIFY_CHECK(j <= n);
block = secp256k1_rss_binom(n - j, remaining - 1);
if (rank < block) {
subset |= (secp256k1_rss_subset)(1u << j);
remaining--;
} else {
rank -= block;
}
j++;
}
return subset;
}
/* The members of a subset, ascending, as the byte array the polynomial layer
* expects. Returns how many were written. */
static size_t secp256k1_rss_subset_members(unsigned char *members, secp256k1_rss_subset subset, unsigned int n) {
size_t count = 0;
unsigned int j;
for (j = 1; j <= n; j++) {
if (subset & (secp256k1_rss_subset)(1u << j)) {
members[count++] = (unsigned char)j;
}
}
return count;
}
/* Initializes SHA256 with fixed midstate. This midstate was computed by applying
* SHA256 to SHA256("VPSS/prf")||SHA256("VPSS/prf"). */
static void secp256k1_rss_prf_sha256_tagged(secp256k1_sha256 *sha) {
static const uint32_t midstate[8] = {
0x2c0fc184ul, 0x5cc276f5ul, 0x96930a47ul, 0x1991257eul,
0x5b0bb737ul, 0x8786890cul, 0x875ba8bbul, 0x6b6162bbul
};
secp256k1_sha256_initialize_midstate(sha, 64, midstate);
}
static void secp256k1_rss_prf(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seed32, const unsigned char *w, size_t wlen) {
secp256k1_sha256 sha;
unsigned char buf[32];
secp256k1_rss_prf_sha256_tagged(&sha);
secp256k1_sha256_write(hash_ctx, &sha, seed32, 32);
secp256k1_sha256_write(hash_ctx, &sha, w, wlen);
secp256k1_sha256_finalize(hash_ctx, &sha, buf);
/* Reduce on overflow instead of rejecting. The bias is negligible and the
* reference implementation does the same, which matters because the test
* vectors have to agree byte for byte. */
secp256k1_scalar_set_b32(r, buf, NULL);
secp256k1_memclear_explicit(buf, sizeof(buf));
secp256k1_sha256_clear(&sha);
}
static size_t secp256k1_rss_lagrange_weights_var(secp256k1_scalar *weights, unsigned int n, unsigned int t, unsigned int k) {
secp256k1_scalar denominators[SECP256K1_ICEBERG_MAX_SUMMANDS];
unsigned char members[SECP256K1_ICEBERG_MAX_T];
uint32_t total = secp256k1_rss_binom(n, t - 1);
uint32_t rank;
size_t i, count = 0;
VERIFY_CHECK(t >= 1 && k >= 1 && k <= n);
VERIFY_CHECK(n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
/* The scheme's own bound, and what keeps a subset inside members[]: a
* quorum of 2t-1 has to fit in the group, so t-1 < MAX_T. */
VERIFY_CHECK(2 * t - 1 <= n);
/* Walk every subset in rank order and keep the ones this participant holds
* a seed for, which is exactly those it does not belong to. Iterating the
* global order, instead of enumerating the complement directly, is what
* guarantees seeds and weights stay in step with the serialized share.
*
* The weights come out as fractions and are divided through at the end, one
* batch inversion for the whole share rather than one per weight. */
for (rank = 0; rank < total; rank++) {
secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(n, t - 1, rank);
size_t size;
if (subset & (secp256k1_rss_subset)(1u << k)) {
continue;
}
size = secp256k1_rss_subset_members(members, subset, n);
/* L_a(k): exclude nothing, evaluate at k. */
secp256k1_scalarpoly_lagrange_parts_var(&weights[count], &denominators[count], members, size, 0, k);
count++;
}
VERIFY_CHECK(count == secp256k1_rss_binom(n - 1, t - 1));
/* C(n-1, t-1) is never zero, but the batch inverter reads its first element
* unconditionally, so guard the empty run anyway. */
if (count > 0) {
secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, count);
for (i = 0; i < count; i++) {
secp256k1_scalar_mul(&weights[i], &weights[i], &denominators[i]);
}
}
return count;
}
static void secp256k1_rss_eval(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seeds, const secp256k1_scalar *weights, size_t num, const unsigned char *w, size_t wlen) {
secp256k1_scalar term;
size_t i;
secp256k1_scalar_set_int(r, 0);
for (i = 0; i < num; i++) {
secp256k1_rss_prf(hash_ctx, &term, &seeds[32 * i], w, wlen);
/* Secret times public. The loop bound and the stride are both public,
* so nothing here branches or indexes on a seed value. */
secp256k1_scalar_mul(&term, &term, &weights[i]);
secp256k1_scalar_add(r, r, &term);
}
secp256k1_scalar_clear(&term);
}
#endif /* SECP256K1_MODULE_ICEBERG_RSS_IMPL_H */

View File

@@ -0,0 +1,89 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_SCALAR_POLY_H
#define SECP256K1_MODULE_ICEBERG_SCALAR_POLY_H
#include "../../scalar.h"
/* Polynomials over the scalar field, represented as dense coefficient arrays
* with coeff[i] the coefficient of x^i.
*
* This layer knows nothing about elliptic curves, hashing, or threshold
* signing. It is a separate file so the Lagrange machinery can be audited
* against the paper on its own.
*
* Everything is variable time in the indices, which are participant numbers and
* therefore public. Coefficient values are touched with constant-time scalar
* operations, with one exception: every routine that inverts ends at
* secp256k1_scalar_inverse_var and is variable time in the value it inverts.
* That is lagrange_eval_var directly, and lagrange_basis_var and interpolate_at0
* through inverse_batch_var. What they invert is always a Lagrange denominator,
* built from indices alone. A secret may pass through interpolate_at0's vals and
* through the multiply-and-add routines; a secret must never reach an
* inversion. */
/* An interpolation may involve at most this many points. Participant counts are
* bounded far below this by the C(n, t-1) share blowup; the slack costs only
* stack space in the basis computation. */
#define SECP256K1_SCALARPOLY_MAX_POINTS 16
/* A batch inversion may involve at most this many scalars. It is not the point
* bound: the largest batch is one weight per seed a participant holds, which is
* C(n-1, t-1) and an order of magnitude larger. The consumer checks its own
* bound against this one at compile time. */
#define SECP256K1_SCALARPOLY_MAX_BATCH 128
/* coeffs[0..m] <- prod_i (x - roots[i]), monic, so coeffs[m] is one.
* roots must be distinct and hold m entries; coeffs must hold m+1. */
static void secp256k1_scalarpoly_from_roots_var(secp256k1_scalar *coeffs, const unsigned char *roots, size_t m);
/* q[0..m-1] <- a(x) / (x - root), for monic a of degree m with a(root) == 0.
* The remainder is checked under VERIFY; a caller passing a root that is not a
* root has a bug, not a runtime error. */
static void secp256k1_scalarpoly_div_root_var(secp256k1_scalar *q, const secp256k1_scalar *a, size_t m, unsigned char root);
/* r <- prod_{i in idx, i != exclude} (i - at) / (i - exclude).
*
* One function covers both places Lagrange weights appear:
*
* exclude = 0, at = k gives L_a(k), the replicated-sharing weight that
* vanishes on every member of the set a
* exclude = j, at = 0 gives lambda_j, the weight that reconstructs f(0)
*
* They look unrelated in the papers and are the same product.
*
* The one-weight form. Anything computing many weights at once goes
* through lagrange_parts_var below instead and inverts them in a batch, so the
* one caller of this is on_degree_var, which needs a handful. */
static void secp256k1_scalarpoly_lagrange_eval_var(secp256k1_scalar *r, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at);
/* The same weight, left as a fraction: num/den, with no inversion performed.
* Inverting dominates the cost of a weight, so a caller computing many at once
* should collect the denominators and invert the batch. */
static void secp256k1_scalarpoly_lagrange_parts_var(secp256k1_scalar *num, secp256k1_scalar *den, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at);
/* basis[j*m + i] <- coefficient of x^i in the jth Lagrange basis polynomial for
* the node set idx. Needed only by the degree check, which inspects the high
* coefficients; the point evaluations above are cheaper for everything else. */
static void secp256k1_scalarpoly_lagrange_basis_var(secp256k1_scalar *basis, const unsigned char *idx, size_t m);
/* r <- sum_j lambda_j * vals[j], the interpolation of the given points at zero.
* idx is public and drives variable-time work; vals may be secret. */
static void secp256k1_scalarpoly_interpolate_at0(secp256k1_scalar *r, const unsigned char *idx, const secp256k1_scalar *vals, size_t m);
/* Do the m points (idx[i], vals[i]) lie on one polynomial of degree at most
* t-1? The first t of them fix that polynomial, so the question is only whether
* the other m-t agree with it, and at m == t there is nothing to ask.
*
* Both idx and vals must be public: this is variable time in the values, and it
* exists to check contributions that have already been published. */
static int secp256k1_scalarpoly_on_degree_var(const unsigned char *idx, const secp256k1_scalar *vals, size_t m, size_t t);
/* r[0..len-1] <- a[i]^-1, using one field inversion for the whole batch.
* All inputs must be non-zero. r and a may alias. */
static void secp256k1_scalarpoly_inverse_batch_var(secp256k1_scalar *r, const secp256k1_scalar *a, size_t len);
#endif /* SECP256K1_MODULE_ICEBERG_SCALAR_POLY_H */

View File

@@ -0,0 +1,223 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_SCALAR_POLY_IMPL_H
#define SECP256K1_MODULE_ICEBERG_SCALAR_POLY_IMPL_H
#include "scalar_poly.h"
#include "../../scalar.h"
#include "../../util.h"
/* Participant indices are small positive integers, but differences between them
* are signed. Accumulating those differences in the scalar field instead of in
* a C integer keeps us out of overflow-analysis territory entirely: with indices
* up to 255 and sixteen points, an integer product would reach 2^120. */
static void secp256k1_scalarpoly_small_diff(secp256k1_scalar *r, unsigned int a, unsigned int b) {
if (a >= b) {
secp256k1_scalar_set_int(r, a - b);
} else {
secp256k1_scalar_set_int(r, b - a);
secp256k1_scalar_negate(r, r);
}
}
static void secp256k1_scalarpoly_from_roots_var(secp256k1_scalar *coeffs, const unsigned char *roots, size_t m) {
secp256k1_scalar neg_root, term;
size_t i, j;
VERIFY_CHECK(m <= SECP256K1_SCALARPOLY_MAX_POINTS);
/* Start from the constant polynomial 1 and multiply in one root at a time.
* Multiplying by (x - r) shifts every coefficient up and subtracts r times
* the original, so we walk downwards to avoid clobbering what we still need. */
secp256k1_scalar_set_int(&coeffs[0], 1);
for (i = 0; i < m; i++) {
secp256k1_scalarpoly_small_diff(&neg_root, 0, roots[i]);
coeffs[i + 1] = coeffs[i];
for (j = i; j > 0; j--) {
secp256k1_scalar_mul(&term, &coeffs[j], &neg_root);
secp256k1_scalar_add(&coeffs[j], &coeffs[j - 1], &term);
}
secp256k1_scalar_mul(&coeffs[0], &coeffs[0], &neg_root);
}
}
static void secp256k1_scalarpoly_div_root_var(secp256k1_scalar *q, const secp256k1_scalar *a, size_t m, unsigned char root) {
secp256k1_scalar r, term;
size_t i;
VERIFY_CHECK(m >= 1 && m <= SECP256K1_SCALARPOLY_MAX_POINTS);
secp256k1_scalar_set_int(&r, root);
/* Synthetic division: q[i-1] = a[i] + root*q[i], seeded with the leading
* coefficient. What is left over is a(root), so the division is exact
* precisely when root is a root of a; whether a is monic plays no part. */
q[m - 1] = a[m];
for (i = m - 1; i > 0; i--) {
secp256k1_scalar_mul(&term, &q[i], &r);
secp256k1_scalar_add(&q[i - 1], &a[i], &term);
}
#ifdef VERIFY
/* The remainder must vanish. If it does not, the caller passed a value that
* is not a root of a, which no correct call site can do. */
secp256k1_scalar_mul(&term, &q[0], &r);
secp256k1_scalar_add(&term, &term, &a[0]);
VERIFY_CHECK(secp256k1_scalar_is_zero(&term));
#endif
}
static void secp256k1_scalarpoly_lagrange_parts_var(secp256k1_scalar *num, secp256k1_scalar *den, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at) {
secp256k1_scalar factor;
size_t i;
secp256k1_scalar_set_int(num, 1);
secp256k1_scalar_set_int(den, 1);
for (i = 0; i < m; i++) {
if (idx[i] == exclude) {
continue;
}
secp256k1_scalarpoly_small_diff(&factor, idx[i], at);
secp256k1_scalar_mul(num, num, &factor);
secp256k1_scalarpoly_small_diff(&factor, idx[i], exclude);
secp256k1_scalar_mul(den, den, &factor);
}
}
static void secp256k1_scalarpoly_lagrange_eval_var(secp256k1_scalar *r, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at) {
secp256k1_scalar numerator, denominator;
secp256k1_scalarpoly_lagrange_parts_var(&numerator, &denominator, idx, m, exclude, at);
/* Distinct indices make every factor non-zero.
* secp256k1_scalar_inverse_var maps zero to zero instead of failing, so a
* duplicate index would otherwise produce a silently wrong weight. */
VERIFY_CHECK(!secp256k1_scalar_is_zero(&denominator));
secp256k1_scalar_inverse_var(&denominator, &denominator);
secp256k1_scalar_mul(r, &numerator, &denominator);
}
static int secp256k1_scalarpoly_on_degree_var(const unsigned char *idx, const secp256k1_scalar *vals, size_t m, size_t t) {
size_t j, k;
VERIFY_CHECK(t >= 1 && m >= t);
for (k = t; k < m; k++) {
secp256k1_scalar expected, weight, term;
secp256k1_scalar_set_int(&expected, 0);
for (j = 0; j < t; j++) {
/* The weight of node j in the interpolant of the first t points,
* evaluated at the point being tested. idx[k] is not one of those
* nodes, since the caller has already refused a repeated index. */
secp256k1_scalarpoly_lagrange_eval_var(&weight, idx, t, idx[j], idx[k]);
secp256k1_scalar_mul(&term, &weight, &vals[j]);
secp256k1_scalar_add(&expected, &expected, &term);
}
if (!secp256k1_scalar_eq(&expected, &vals[k])) {
return 0;
}
}
return 1;
}
static void secp256k1_scalarpoly_lagrange_basis_var(secp256k1_scalar *basis, const unsigned char *idx, size_t m) {
secp256k1_scalar master[SECP256K1_SCALARPOLY_MAX_POINTS + 1];
secp256k1_scalar denominators[SECP256K1_SCALARPOLY_MAX_POINTS];
secp256k1_scalar factor;
size_t j, i;
VERIFY_CHECK(m >= 1 && m <= SECP256K1_SCALARPOLY_MAX_POINTS);
/* Fill the tail as well as the m entries set in the loop below. Only the
* first m are ever read, but inverse_batch_var accepts a len up to
* SECP256K1_SCALARPOLY_MAX_BATCH and GCC cannot see that m is bounded by
* the smaller SECP256K1_SCALARPOLY_MAX_POINTS, so it warns on the
* partially filled array. */
for (j = 0; j < SECP256K1_SCALARPOLY_MAX_POINTS; j++) {
secp256k1_scalar_set_int(&denominators[j], 1);
}
/* Every basis polynomial is the master polynomial with one root removed, so
* build the master once and divide it down m times, instead of forming m
* products from scratch. */
secp256k1_scalarpoly_from_roots_var(master, idx, m);
for (j = 0; j < m; j++) {
secp256k1_scalarpoly_div_root_var(&basis[j * m], master, m, idx[j]);
secp256k1_scalar_set_int(&denominators[j], 1);
for (i = 0; i < m; i++) {
if (i == j) {
continue;
}
secp256k1_scalarpoly_small_diff(&factor, idx[j], idx[i]);
secp256k1_scalar_mul(&denominators[j], &denominators[j], &factor);
}
}
secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, m);
for (j = 0; j < m; j++) {
for (i = 0; i < m; i++) {
secp256k1_scalar_mul(&basis[j * m + i], &basis[j * m + i], &denominators[j]);
}
}
}
static void secp256k1_scalarpoly_interpolate_at0(secp256k1_scalar *r, const unsigned char *idx, const secp256k1_scalar *vals, size_t m) {
secp256k1_scalar numerators[SECP256K1_SCALARPOLY_MAX_POINTS];
secp256k1_scalar denominators[SECP256K1_SCALARPOLY_MAX_POINTS];
secp256k1_scalar term;
size_t j;
VERIFY_CHECK(m >= 1 && m <= SECP256K1_SCALARPOLY_MAX_POINTS);
/* Fill the tail as well: only the first m entries are read, but GCC cannot
* see that and warns on the partially filled array. */
for (j = 0; j < SECP256K1_SCALARPOLY_MAX_POINTS; j++) {
secp256k1_scalar_set_int(&denominators[j], 1);
}
/* One inversion for the whole quorum rather than one per weight. */
for (j = 0; j < m; j++) {
secp256k1_scalarpoly_lagrange_parts_var(&numerators[j], &denominators[j],
idx, m, idx[j], 0);
}
secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, m);
secp256k1_scalar_set_int(r, 0);
for (j = 0; j < m; j++) {
secp256k1_scalar_mul(&term, &numerators[j], &denominators[j]);
secp256k1_scalar_mul(&term, &term, &vals[j]);
secp256k1_scalar_add(r, r, &term);
}
secp256k1_scalar_clear(&term);
}
static void secp256k1_scalarpoly_inverse_batch_var(secp256k1_scalar *r, const secp256k1_scalar *a, size_t len) {
secp256k1_scalar prefix[SECP256K1_SCALARPOLY_MAX_BATCH];
secp256k1_scalar running;
size_t i;
VERIFY_CHECK(len >= 1 && len <= SECP256K1_SCALARPOLY_MAX_BATCH);
/* Montgomery's trick: invert the product of everything once, then peel the
* individual inverses off using the running prefix products. */
prefix[0] = a[0];
for (i = 1; i < len; i++) {
secp256k1_scalar_mul(&prefix[i], &prefix[i - 1], &a[i]);
}
VERIFY_CHECK(!secp256k1_scalar_is_zero(&prefix[len - 1]));
secp256k1_scalar_inverse_var(&running, &prefix[len - 1]);
for (i = len - 1; i > 0; i--) {
secp256k1_scalar this_inverse;
secp256k1_scalar_mul(&this_inverse, &running, &prefix[i - 1]);
secp256k1_scalar_mul(&running, &running, &a[i]);
r[i] = this_inverse;
}
r[0] = running;
}
#endif /* SECP256K1_MODULE_ICEBERG_SCALAR_POLY_IMPL_H */

View File

@@ -0,0 +1,842 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_SESSION_IMPL_H
#define SECP256K1_MODULE_ICEBERG_SESSION_IMPL_H
#include <string.h>
#include "../../../include/secp256k1_iceberg.h"
#include "keygen_impl.h"
#include "rss_impl.h"
#include "scalar_poly_impl.h"
#include "vpss_impl.h"
#include "../musig/keyagg.h"
#include "../musig/session.h"
#include "../../group.h"
#include "../../hash.h"
#include "../../scalar.h"
#include "../../util.h"
static const unsigned char secp256k1_iceberg_pubnonce_magic[4] = { 0x1c, 0xeb, 0x60, 0x2e };
static const unsigned char secp256k1_iceberg_aggnonce_magic[4] = { 0x1c, 0xeb, 0xa9, 0x77 };
static const unsigned char secp256k1_iceberg_psig_magic[4] = { 0x1c, 0xeb, 0x51, 0x8b };
/* Initializes SHA256 with fixed midstate. This midstate was computed by applying
* SHA256 to SHA256("Iceberg/noncecoef")||SHA256("Iceberg/noncecoef"). */
static void secp256k1_iceberg_noncecoef_sha256_tagged(secp256k1_sha256 *sha) {
static const uint32_t midstate[8] = {
0x8848611aul, 0x6abc622ful, 0x7d0bbf3cul, 0x9d9c84faul,
0x0188712dul, 0x04573e61ul, 0x23eb12e6ul, 0x88a27ee0ul
};
secp256k1_sha256_initialize_midstate(sha, 64, midstate);
}
/* The two VPSS labels a session uses, big-endian 1 and 2 followed by the
* session label. Sixty-four bytes each, matching the reference exactly. */
static void secp256k1_iceberg_nonce_label(unsigned char *out64, unsigned int which, const unsigned char *sid32) {
memset(out64, 0, 32);
out64[31] = (unsigned char)which;
memcpy(&out64[32], sid32, 32);
}
/* This participant's own two nonce points for a session: its share of each of
* the two nonce sharings, in the group.
*
* Round one publishes these; round two derives them again to check the set it
* was handed at its own index. That check means anything at all only if the two
* rounds agree on the derivation, so they use this rather than a copy each. The
* caller must have checked that the context carries an ecmult_gen table.
*
* k_out, if not NULL, receives the two scalars the points commit to. They are
* secret, and a caller that asks for them owns them and must clear them. Pass
* NULL to have them cleared here.
*
* The points are about to be published either way, so they are declassified. */
static void secp256k1_iceberg_own_nonce_points(const secp256k1_context *ctx, secp256k1_ge *pts, secp256k1_scalar *k_out, const unsigned char *seeds, const secp256k1_scalar *weights, size_t count, const unsigned char *sid32) {
unsigned char label[64];
int i;
for (i = 0; i < 2; i++) {
secp256k1_scalar k_i;
secp256k1_gej pj;
secp256k1_iceberg_nonce_label(label, (unsigned int)(i + 1), sid32);
secp256k1_rss_eval(secp256k1_get_hash_context(ctx), &k_i, seeds, weights, count, label, sizeof(label));
secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &pj, &k_i);
secp256k1_ge_set_gej(&pts[i], &pj);
secp256k1_declassify(ctx, &pts[i], sizeof(pts[i]));
if (k_out != NULL) {
k_out[i] = k_i;
}
secp256k1_scalar_clear(&k_i);
}
}
/* b1 = H_Iceberg/noncecoef(R1 || R2' || P): the unscaled pre-nonces, then the
* group's key, each in musig's 33-byte extended encoding. The paper writes the
* arguments key first; the reference serializes them in this order and so do
* we, which the cross-implementation vectors pin.
*
* An inner aggregate has no binding of its own and the outer coefficient is
* computed too late to supply one, so every nesting level contributes a factor.
* Computed here and nowhere else, so the preimage moves in one place. */
static void secp256k1_iceberg_noncecoef(const secp256k1_context *ctx, secp256k1_scalar *b1, secp256k1_ge *nonce_pts, const secp256k1_ge *group_pk) {
secp256k1_sha256 sha;
secp256k1_ge pk = *group_pk;
unsigned char buf[33];
unsigned char out[32];
secp256k1_iceberg_noncecoef_sha256_tagged(&sha);
secp256k1_musig_ge_serialize_ext(buf, &nonce_pts[0]);
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, buf, sizeof(buf));
secp256k1_musig_ge_serialize_ext(buf, &nonce_pts[1]);
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, buf, sizeof(buf));
secp256k1_musig_ge_serialize_ext(buf, &pk);
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, buf, sizeof(buf));
secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &sha, out);
secp256k1_scalar_set_b32(b1, out, NULL);
}
/* The pair the group publishes, (R1, b1*R2'), from the internal one. Only the
* second point is scaled: at nesting depth one the coefficient enters as
* b1^(i-1), so the first is carried through untouched.
*
* Round one publishes this and round two rebuilds it from the aggregate it
* derives from the contributions. The two have to agree exactly or every
* signature the group produces is invalid, so it is written once rather than
* twice.
*
* 0 if either point comes out at infinity, which a MuSig2 public nonce has no
* encoding for. Both rounds refuse such a set because both reach it here: a
* contribution set can be consistent, agree with a signer's own contribution,
* and still interpolate to infinity at zero. */
static int secp256k1_iceberg_publish_nonce(secp256k1_ge *out, const secp256k1_ge *pre, const secp256k1_scalar *b1) {
secp256k1_gej r2j, scaled;
out[0] = pre[0];
secp256k1_gej_set_ge(&r2j, &pre[1]);
secp256k1_ecmult(&scaled, &r2j, b1, NULL);
secp256k1_ge_set_gej(&out[1], &scaled);
return !secp256k1_ge_is_infinity(&out[0]) && !secp256k1_ge_is_infinity(&out[1]);
}
static void secp256k1_iceberg_pubnonce_save(secp256k1_iceberg_pubnonce *nonce, unsigned int k, const secp256k1_ge *pts) {
memset(nonce->data, 0, sizeof(nonce->data));
memcpy(nonce->data, secp256k1_iceberg_pubnonce_magic, 4);
nonce->data[4] = (unsigned char)k;
secp256k1_ge_to_bytes_ext(&nonce->data[5], &pts[0]);
secp256k1_ge_to_bytes_ext(&nonce->data[69], &pts[1]);
}
static int secp256k1_iceberg_pubnonce_load(const secp256k1_context *ctx, unsigned int *k, secp256k1_ge *pts, const secp256k1_iceberg_pubnonce *nonce) {
ARG_CHECK(secp256k1_memcmp_var(nonce->data, secp256k1_iceberg_pubnonce_magic, 4) == 0);
*k = nonce->data[4];
ARG_CHECK(*k >= 1 && *k <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
secp256k1_ge_from_bytes_ext(&pts[0], &nonce->data[5]);
secp256k1_ge_from_bytes_ext(&pts[1], &nonce->data[69]);
return 1;
}
static void secp256k1_iceberg_aggnonce_save(secp256k1_iceberg_aggnonce *nonce, const secp256k1_ge *pts) {
memset(nonce->data, 0, sizeof(nonce->data));
memcpy(nonce->data, secp256k1_iceberg_aggnonce_magic, 4);
secp256k1_ge_to_bytes_ext(&nonce->data[4], &pts[0]);
secp256k1_ge_to_bytes_ext(&nonce->data[68], &pts[1]);
}
static int secp256k1_iceberg_aggnonce_load(const secp256k1_context *ctx, secp256k1_ge *pts, const secp256k1_iceberg_aggnonce *nonce) {
ARG_CHECK(secp256k1_memcmp_var(nonce->data, secp256k1_iceberg_aggnonce_magic, 4) == 0);
secp256k1_ge_from_bytes_ext(&pts[0], &nonce->data[4]);
secp256k1_ge_from_bytes_ext(&pts[1], &nonce->data[68]);
return 1;
}
/* Read a set of nonce contributions: the index and both nonce points of each,
* refusing an index outside the group, a repeated index, or points that do not
* lie on a single sharing of degree t-1. */
static int secp256k1_iceberg_contributions_load(const secp256k1_context *ctx, unsigned char *idx, secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS], const secp256k1_iceberg_pubnonce * const *pubnonces, size_t m, unsigned int n, unsigned int t) {
size_t i, j;
if (n < 1 || n > SECP256K1_ICEBERG_MAX_PARTICIPANTS || t < 1 || t > (n + 1) / 2) {
return 0;
}
if (m < 2 * (size_t)t - 1 || m > n) {
return 0;
}
for (i = 0; i < m; i++) {
secp256k1_ge pts[2];
unsigned int who;
if (!secp256k1_iceberg_pubnonce_load(ctx, &who, pts, pubnonces[i])) {
return 0;
}
/* An index above n names no member of this group, so nothing could have
* authenticated what it carries. */
if (who > n) {
return 0;
}
idx[i] = (unsigned char)who;
for (j = 0; j < i; j++) {
if (idx[j] == idx[i]) {
return 0;
}
}
points[0][i] = pts[0];
points[1][i] = pts[1];
}
for (i = 0; i < 2; i++) {
if (!secp256k1_vpss_verify_var(ctx, idx, points[i], m, t)) {
return 0;
}
}
return 1;
}
/* Both sharings interpolated back to their constant term: the group's internal
* (R1, R2'). Only meaningful on a set contributions_load has accepted. */
static int secp256k1_iceberg_contributions_combine(const secp256k1_context *ctx, secp256k1_ge *combined, const unsigned char *idx, secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS], size_t m) {
secp256k1_gej sum;
int i;
for (i = 0; i < 2; i++) {
if (!secp256k1_vpss_combine_var(ctx, &sum, idx, points[i], m)) {
return 0;
}
secp256k1_ge_set_gej(&combined[i], &sum);
}
return 1;
}
/* Wire formats.
*
* pubnonce 67 = index(1) | point(33) | point(33)
* aggnonce 66 = point(33) | point(33)
* partial_sig 33 = index(1) | scalar(32)
*
* Points use musig's extended serializer, which encodes infinity as 33 zero
* bytes. The aggregate can reach infinity, so the format has to express it, and
* a member's contribution uses the same encoding so the two do not differ
* without a reason.
*
* A member's objects carry its index inside the encoding, because everything
* downstream is indexed and pairing the two is then not something a caller can
* get wrong on the wire. The group's aggregate belongs to no member and carries
* none, which is the whole of the 67-versus-66 difference. */
int secp256k1_iceberg_pubnonce_serialize(const secp256k1_context *ctx, unsigned char *out67, const secp256k1_iceberg_pubnonce *pubnonce) {
secp256k1_ge pts[2];
unsigned int k;
int i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(out67 != NULL);
memset(out67, 0, 67);
ARG_CHECK(pubnonce != NULL);
if (!secp256k1_iceberg_pubnonce_load(ctx, &k, pts, pubnonce)) {
return 0;
}
out67[0] = (unsigned char)k;
for (i = 0; i < 2; i++) {
secp256k1_musig_ge_serialize_ext(&out67[1 + 33 * i], &pts[i]);
}
return 1;
}
int secp256k1_iceberg_pubnonce_parse(const secp256k1_context *ctx, secp256k1_iceberg_pubnonce *pubnonce, const unsigned char *in67) {
secp256k1_ge pts[2];
int i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(pubnonce != NULL);
memset(pubnonce, 0, sizeof(*pubnonce));
ARG_CHECK(in67 != NULL);
if (in67[0] < 1 || in67[0] > SECP256K1_ICEBERG_MAX_PARTICIPANTS) {
return 0;
}
for (i = 0; i < 2; i++) {
if (!secp256k1_musig_ge_parse_ext(&pts[i], &in67[1 + 33 * i])) {
return 0;
}
}
secp256k1_iceberg_pubnonce_save(pubnonce, in67[0], pts);
return 1;
}
int secp256k1_iceberg_aggnonce_serialize(const secp256k1_context *ctx, unsigned char *out66, const secp256k1_iceberg_aggnonce *aggnonce) {
secp256k1_ge pts[2];
int i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(out66 != NULL);
memset(out66, 0, 66);
ARG_CHECK(aggnonce != NULL);
if (!secp256k1_iceberg_aggnonce_load(ctx, pts, aggnonce)) {
return 0;
}
for (i = 0; i < 2; i++) {
secp256k1_musig_ge_serialize_ext(&out66[33 * i], &pts[i]);
}
return 1;
}
int secp256k1_iceberg_aggnonce_parse(const secp256k1_context *ctx, secp256k1_iceberg_aggnonce *aggnonce, const unsigned char *in66) {
secp256k1_ge pts[2];
int i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(aggnonce != NULL);
memset(aggnonce, 0, sizeof(*aggnonce));
ARG_CHECK(in66 != NULL);
for (i = 0; i < 2; i++) {
if (!secp256k1_musig_ge_parse_ext(&pts[i], &in66[33 * i])) {
return 0;
}
}
secp256k1_iceberg_aggnonce_save(aggnonce, pts);
return 1;
}
int secp256k1_iceberg_partial_sig_serialize(const secp256k1_context *ctx, unsigned char *out33, const secp256k1_iceberg_partial_sig *partial_sig) {
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(out33 != NULL);
memset(out33, 0, 33);
ARG_CHECK(partial_sig != NULL);
/* ARG_CHECK rather than a plain return, so that a malformed object reaches
* the caller the same way here as it does through the load helpers the four
* sibling serializers use. */
ARG_CHECK(secp256k1_memcmp_var(partial_sig->data, secp256k1_iceberg_psig_magic, 4) == 0);
out33[0] = partial_sig->data[4];
memcpy(&out33[1], &partial_sig->data[5], 32);
return 1;
}
int secp256k1_iceberg_partial_sig_parse(const secp256k1_context *ctx, secp256k1_iceberg_partial_sig *partial_sig, const unsigned char *in33) {
secp256k1_scalar s;
int overflow;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(partial_sig != NULL);
memset(partial_sig, 0, sizeof(*partial_sig));
ARG_CHECK(in33 != NULL);
if (in33[0] < 1 || in33[0] > SECP256K1_ICEBERG_MAX_PARTICIPANTS) {
return 0;
}
/* Reject an out-of-range scalar here instead of reducing it. Aggregation
* is linear, so a share that wrapped would combine into a signature that
* simply fails to verify. */
secp256k1_scalar_set_b32(&s, &in33[1], &overflow);
if (overflow) {
return 0;
}
memcpy(partial_sig->data, secp256k1_iceberg_psig_magic, 4);
partial_sig->data[4] = in33[0];
memcpy(&partial_sig->data[5], &in33[1], 32);
secp256k1_scalar_clear(&s);
return 1;
}
int secp256k1_iceberg_nonce_gen(const secp256k1_context *ctx, secp256k1_iceberg_pubnonce *pubnonce, const secp256k1_iceberg_share *share, const secp256k1_iceberg_share_cache *cache, const unsigned char *sid32) {
secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS];
secp256k1_ge pts[2];
const unsigned char *seeds;
unsigned int n, t, k;
size_t count;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(pubnonce != NULL);
memset(pubnonce, 0, sizeof(*pubnonce));
ARG_CHECK(share != NULL);
ARG_CHECK(sid32 != NULL);
ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share);
if (count == 0 || !secp256k1_iceberg_weights_for(ctx, weights, count, n, t, k, cache)) {
return 0;
}
secp256k1_iceberg_own_nonce_points(ctx, pts, NULL, seeds, weights, count, sid32);
secp256k1_iceberg_pubnonce_save(pubnonce, k, pts);
return 1;
}
int secp256k1_iceberg_nonce_agg(const secp256k1_context *ctx, secp256k1_musig_pubnonce *musig_pubnonce, secp256k1_iceberg_aggnonce *aggnonce, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, const secp256k1_pubkey *group_pk) {
unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_ge combined[2], published[2], pk;
secp256k1_scalar b1;
size_t i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(musig_pubnonce != NULL);
memset(musig_pubnonce, 0, sizeof(*musig_pubnonce));
/* Optional. Only the serializer reads an iceberg_aggnonce, so a caller with
* no use for one passes NULL instead of allocating 132 bytes to ignore. */
if (aggnonce != NULL) {
memset(aggnonce, 0, sizeof(*aggnonce));
}
ARG_CHECK(pubnonces != NULL);
ARG_CHECK(group_pk != NULL);
ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
ARG_CHECK(t >= 1 && t <= (n + 1) / 2);
/* Bounded here for the same reason as in partial_sign: the sweep has to stay
* inside the array, and contributions_load checks the quorum itself. */
if (n_pubnonces > n) {
return 0;
}
for (i = 0; i < n_pubnonces; i++) {
ARG_CHECK(pubnonces[i] != NULL);
}
if (!secp256k1_pubkey_load(ctx, &pk, group_pk)) {
return 0;
}
if (!secp256k1_iceberg_contributions_load(ctx, idx, points, pubnonces, n_pubnonces, n, t)) {
return 0;
}
if (!secp256k1_iceberg_contributions_combine(ctx, combined, idx, points, n_pubnonces)) {
return 0;
}
secp256k1_iceberg_noncecoef(ctx, &b1, combined, &pk);
if (!secp256k1_iceberg_publish_nonce(published, combined, &b1)) {
return 0;
}
/* Both out-parameters are written here rather than as each is ready, so that
* every path that returns 0 leaves them as the memset at the top left them. */
if (aggnonce != NULL) {
secp256k1_iceberg_aggnonce_save(aggnonce, combined);
}
secp256k1_musig_pubnonce_save(musig_pubnonce, published);
return 1;
}
/* Everything round two needs from the upper session, recomputed here rather
* than accepted from the caller.
*
* The aggregate nonce is assembled from the group's own exported contribution
* plus the cosigners', so "my nonce is in there" is true by construction. A
* signer that took a prepared session object instead would be trusting values
* an adversary can choose.
*/
static int secp256k1_iceberg_session_values(const secp256k1_context *ctx, secp256k1_scalar *b0b1, secp256k1_scalar *key_coef, int *fin_parity, const secp256k1_iceberg_aggnonce *aggnonce, const secp256k1_pubkey *group_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const unsigned char *msg32, const secp256k1_musig_aggnonce *cosigner_aggnonce) {
secp256k1_keyagg_cache_internal cache_i;
secp256k1_ge group_pts[2], cosigner_pts[2], total[2], pk;
secp256k1_scalar b1, b0, a, e;
secp256k1_gej acc;
unsigned char agg_pk32[32], fin_nonce[32];
int i;
if (!secp256k1_keyagg_cache_load(ctx, &cache_i, keyagg_cache)) {
return 0;
}
if (!secp256k1_pubkey_load(ctx, &pk, group_pk)) {
return 0;
}
if (!secp256k1_iceberg_aggnonce_load(ctx, group_pts, aggnonce)) {
return 0;
}
if (!secp256k1_musig_aggnonce_load(ctx, cosigner_pts, cosigner_aggnonce)) {
return 0;
}
/* Re-derive the group's published nonce with the same function round one
* published it with, then add the cosigners' to it. */
secp256k1_iceberg_noncecoef(ctx, &b1, group_pts, &pk);
if (!secp256k1_iceberg_publish_nonce(total, group_pts, &b1)) {
return 0;
}
for (i = 0; i < 2; i++) {
secp256k1_gej_set_ge(&acc, &total[i]);
secp256k1_gej_add_ge_var(&acc, &acc, &cosigner_pts[i], NULL);
secp256k1_ge_set_gej(&total[i], &acc);
}
secp256k1_fe_get_b32(agg_pk32, &cache_i.pk.x);
secp256k1_musig_nonce_process_internal(ctx, fin_parity, fin_nonce, &b0, total, agg_pk32, msg32);
secp256k1_schnorrsig_challenge(secp256k1_get_hash_context(ctx), &e, fin_nonce, msg32, 32, agg_pk32);
secp256k1_scalar_mul(b0b1, &b0, &b1);
/* The key coefficient carries the aggregation weight and the parity
* bookkeeping from BIP-340: e * a * g * gacc, where the sign flips if the
* aggregate key is odd exactly once against the accumulated parity. */
secp256k1_musig_keyaggcoef(secp256k1_get_hash_context(ctx), &a, &cache_i, &pk);
secp256k1_scalar_mul(key_coef, &e, &a);
if (secp256k1_fe_is_odd(&cache_i.pk.y) != cache_i.parity_acc) {
secp256k1_scalar_negate(key_coef, key_coef);
}
return 1;
}
int secp256k1_iceberg_keyagg_check(const secp256k1_context *ctx, const secp256k1_musig_keyagg_cache *keyagg_cache, const secp256k1_pubkey * const *pubkeys, size_t n_pubkeys, const secp256k1_pubkey *group_pk) {
secp256k1_keyagg_cache_internal given, rebuilt;
secp256k1_musig_keyagg_cache scratch;
secp256k1_ge target;
int found = 0;
size_t i;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(keyagg_cache != NULL);
ARG_CHECK(pubkeys != NULL);
ARG_CHECK(group_pk != NULL);
ARG_CHECK(n_pubkeys >= 1);
for (i = 0; i < n_pubkeys; i++) {
ARG_CHECK(pubkeys[i] != NULL);
}
if (!secp256k1_keyagg_cache_load(ctx, &given, keyagg_cache)) {
return 0;
}
if (!secp256k1_musig_pubkey_agg(ctx, NULL, &scratch, pubkeys, n_pubkeys)) {
return 0;
}
if (!secp256k1_keyagg_cache_load(ctx, &rebuilt, &scratch)) {
return 0;
}
/* pks_hash is the hash of the key list, fixed when the list is aggregated
* and untouched by any tweak applied afterwards, so this says the cache
* aggregates this list whatever has since been tweaked onto it. Rebuilding
* the cache from the list and using that instead would not do: the tweaks
* are not recoverable from the list, and it is the caller's cache that
* partial_sign will be working against.
*
* second_pk is also in the cache and is also fixed at aggregation, but it is
* derived from the same list, so comparing it as well could only fail on a
* hash collision. */
if (secp256k1_memcmp_var(given.pks_hash, rebuilt.pks_hash, 32) != 0) {
return 0;
}
if (!secp256k1_pubkey_load(ctx, &target, group_pk)) {
return 0;
}
for (i = 0; i < n_pubkeys; i++) {
secp256k1_ge candidate;
if (!secp256k1_pubkey_load(ctx, &candidate, pubkeys[i])) {
return 0;
}
if (secp256k1_ge_eq_var(&candidate, &target)) {
found = 1;
}
}
return found;
}
/* Rebuild the group's aggregate nonce from the contributions, and hand back the
* polynomial's value at `index`. Nothing is refused on that value here, and the
* two callers put it to opposite uses. Signing compares it against the
* contribution the signer derives for itself, in
* secp256k1_iceberg_verified_aggnonce below. Verification holds no share to
* derive one from and takes the value as the member's own nonce, which is what
* lets it check a member that published nothing in round one.
*
* A signer that accepts an aggregate from its coordinator is trusting a value it
* cannot check, and the nesting coefficient is a hash of exactly that value.
* Three fabricated aggregates under one label yield three equations in the same
* three unknowns; the third is the key. Deriving it here instead is what Arctic
* does, and the reason it does it. */
static int secp256k1_iceberg_aggnonce_from(const secp256k1_context *ctx, secp256k1_iceberg_aggnonce *aggnonce, secp256k1_ge *at_index, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, unsigned int index) {
unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_ge combined[2];
secp256k1_gej sum;
int i;
if (!secp256k1_iceberg_contributions_load(ctx, idx, points, pubnonces, n_pubnonces, n, t)) {
return 0;
}
/* The degree check proves the set lies on one polynomial of the right
* degree, not which polynomial: a consistent set may be some other session's
* sharing. Evaluating at `index` gives the caller the one value that ties the
* set to a particular label.
*
* A caller that compares it deliberately does not have to have contributed.
* A member who was offline still holds the shares that fix what its
* contribution would have been. Requiring its presence would be stronger
* against an unauthenticated transport and would lock the offline member out;
* under the authenticated transport the scheme assumes it buys nothing,
* because among 2t-1 contributions that all name members, with at most t-1
* corrupt, t are honest and t points already pin the polynomial. */
for (i = 0; i < 2; i++) {
if (!secp256k1_vpss_eval_at_var(ctx, &sum, idx, points[i], n_pubnonces, index)) {
return 0;
}
secp256k1_ge_set_gej(&at_index[i], &sum);
}
if (!secp256k1_iceberg_contributions_combine(ctx, combined, idx, points, n_pubnonces)) {
return 0;
}
secp256k1_iceberg_aggnonce_save(aggnonce, combined);
return 1;
}
/* The signer's use of the above, and the only place that value is compared: the
* set must agree, at this participant's index, with the contribution the
* participant derives for itself. */
static int secp256k1_iceberg_verified_aggnonce(const secp256k1_context *ctx, secp256k1_iceberg_aggnonce *aggnonce, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, unsigned int own_index, const secp256k1_ge *own_pts) {
secp256k1_ge mine[2];
int i;
if (!secp256k1_iceberg_aggnonce_from(ctx, aggnonce, mine, pubnonces, n_pubnonces, n, t, own_index)) {
return 0;
}
for (i = 0; i < 2; i++) {
if (!secp256k1_ge_eq_var(&mine[i], &own_pts[i])) {
return 0;
}
}
return 1;
}
/* The secrets secp256k1_iceberg_partial_sign holds. d and s are not yet
* meaningful on its early error paths; clearing them there writes zeros over
* whatever the stack held, which is what those paths want anyway. */
static void secp256k1_iceberg_partial_sign_clear(secp256k1_scalar *nonce_k, secp256k1_scalar *d, secp256k1_scalar *s) {
secp256k1_scalar_clear(&nonce_k[0]);
secp256k1_scalar_clear(&nonce_k[1]);
secp256k1_scalar_clear(d);
secp256k1_scalar_clear(s);
}
int secp256k1_iceberg_partial_sign(const secp256k1_context *ctx, secp256k1_iceberg_partial_sig *partial_sig, const secp256k1_iceberg_share *share, const secp256k1_iceberg_share_cache *cache, const unsigned char *sid32, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, const secp256k1_pubkey *group_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const unsigned char *msg32, const secp256k1_musig_aggnonce *cosigner_aggnonce) {
secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS];
secp256k1_scalar nonce_k[2], d, s, b0b1, key_coef;
secp256k1_iceberg_aggnonce aggnonce;
secp256k1_ge own_pts[2];
const unsigned char *seeds;
unsigned int n, t, k;
size_t count, j;
int fin_parity;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(partial_sig != NULL);
memset(partial_sig, 0, sizeof(*partial_sig));
ARG_CHECK(share != NULL);
ARG_CHECK(sid32 != NULL);
ARG_CHECK(pubnonces != NULL);
ARG_CHECK(group_pk != NULL);
ARG_CHECK(keyagg_cache != NULL);
ARG_CHECK(msg32 != NULL);
ARG_CHECK(cosigner_aggnonce != NULL);
ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
/* The group size is read before the count is checked, because it is what the
* count has to be checked against. It comes off this participant's own
* share, so no caller and no peer gets to choose it. */
count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share);
if (count == 0) {
return 0;
}
/* The count is peer-influenced, so an impossible one returns 0 the way the
* rest of this call does, and the way the header says it will. It is bounded
* here, ahead of the quorum check in contributions_load, so that the sweep
* below stays inside an array a caller sized from n; a null entry is a caller
* bug and gets the illegal callback instead. partial_sig_verify orders these
* two the same way. */
if (n_pubnonces > n) {
return 0;
}
for (j = 0; j < n_pubnonces; j++) {
ARG_CHECK(pubnonces[j] != NULL);
}
/* sid32 is the caller's to choose and the caller's to keep unique. Three
* responses under one label are three equations in this participant's three
* secrets, and the third of them is its key share. Nothing here can detect
* that. doc/iceberg.md gives the rule in full. */
if (!secp256k1_iceberg_weights_for(ctx, weights, count, n, t, k, cache)) {
return 0;
}
/* Recompute our own contribution so the set can be matched against it, and
* keep the scalars it commits to: they are what this participant signs
* with. */
secp256k1_iceberg_own_nonce_points(ctx, own_pts, nonce_k, seeds, weights, count, sid32);
/* n comes off this participant's own share, so the signer bounds the
* contribution indices against the group it was actually dealt into rather
* than against whatever the coordinator would like the group to be. The
* nonce scalars are live from here on, so every return below clears them. */
if (!secp256k1_iceberg_verified_aggnonce(ctx, &aggnonce, pubnonces, n_pubnonces, n, t, k, own_pts)) {
secp256k1_iceberg_partial_sign_clear(nonce_k, &d, &s);
return 0;
}
if (!secp256k1_iceberg_session_values(ctx, &b0b1, &key_coef, &fin_parity,
&aggnonce, group_pk, keyagg_cache, msg32,
cosigner_aggnonce)) {
secp256k1_iceberg_partial_sign_clear(nonce_k, &d, &s);
return 0;
}
/* The third sharing this participant contributes to, keyed on the fixed
* label rather than the session's. Like the two nonce sharings above it, it
* is recomputed from the seeds rather than carried across from round one, so
* nothing secret had to survive the gap. */
secp256k1_rss_eval(secp256k1_get_hash_context(ctx), &d, seeds, weights, count,
secp256k1_iceberg_keygen_label, sizeof(secp256k1_iceberg_keygen_label));
/* BIP-340 again: if the final nonce came out odd, both nonce terms flip. */
if (fin_parity) {
secp256k1_scalar_negate(&nonce_k[0], &nonce_k[0]);
secp256k1_scalar_negate(&nonce_k[1], &nonce_k[1]);
}
/* s_k = k1 + b0*b1*k2 + e*a*g*gacc*d_k */
secp256k1_scalar_mul(&s, &key_coef, &d);
secp256k1_scalar_mul(&nonce_k[1], &b0b1, &nonce_k[1]);
secp256k1_scalar_add(&s, &s, &nonce_k[1]);
secp256k1_scalar_add(&s, &s, &nonce_k[0]);
memcpy(partial_sig->data, secp256k1_iceberg_psig_magic, 4);
partial_sig->data[4] = (unsigned char)k;
secp256k1_scalar_get_b32(&partial_sig->data[5], &s);
secp256k1_iceberg_partial_sign_clear(nonce_k, &d, &s);
return 1;
}
int secp256k1_iceberg_partial_sig_verify(const secp256k1_context *ctx, const secp256k1_iceberg_partial_sig *partial_sig, const secp256k1_iceberg_pubshare *pubshare, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, const secp256k1_pubkey *group_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const unsigned char *msg32, const secp256k1_musig_aggnonce *cosigner_aggnonce) {
secp256k1_iceberg_aggnonce aggnonce;
secp256k1_scalar b0b1, key_coef, s;
secp256k1_ge nonce_pts[2], d;
secp256k1_gej rj, dj, tmp;
unsigned int k;
size_t i;
int fin_parity;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(partial_sig != NULL);
ARG_CHECK(pubshare != NULL);
ARG_CHECK(pubnonces != NULL);
ARG_CHECK(group_pk != NULL);
ARG_CHECK(keyagg_cache != NULL);
ARG_CHECK(msg32 != NULL);
ARG_CHECK(cosigner_aggnonce != NULL);
ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
ARG_CHECK(t >= 1 && t <= (n + 1) / 2);
/* Bounded here for the same reason as in partial_sign: the sweep has to stay
* inside the array, and contributions_load checks the quorum itself. */
if (n_pubnonces > n) {
return 0;
}
for (i = 0; i < n_pubnonces; i++) {
ARG_CHECK(pubnonces[i] != NULL);
}
if (secp256k1_memcmp_var(partial_sig->data, secp256k1_iceberg_psig_magic, 4) != 0) {
return 0;
}
if (!secp256k1_iceberg_pubshare_load(ctx, &k, &d, pubshare)) {
return 0;
}
if (k > n) {
return 0;
}
/* The public share says which participant is being asked about. A signature
* share carrying some other index is refused rather than verified against
* its own, which catches the two arguments being drawn from different
* members. Not a privacy measure: parse accepts any index in range, so a
* share can be relabeled and tried against every public share until one
* verifies. Authorship was never hidden. */
if (partial_sig->data[4] != k) {
return 0;
}
/* Same aggregate the signer derived, and the participant's own nonce read
* off the same polynomial. Taking it from the set rather than as an argument
* is what lets this check a member who sat out round one: it published no
* contribution, and the set still fixes what its nonce had to be. */
if (!secp256k1_iceberg_aggnonce_from(ctx, &aggnonce, nonce_pts, pubnonces, n_pubnonces, n, t, k)) {
return 0;
}
if (!secp256k1_iceberg_session_values(ctx, &b0b1, &key_coef, &fin_parity,
&aggnonce, group_pk, keyagg_cache, msg32,
cosigner_aggnonce)) {
return 0;
}
/* s_k*G == +-(R1,k + b0*b1*R2,k) + e*a*g*gacc*D_k, rearranged so the whole
* check is one comparison against infinity. */
secp256k1_gej_set_ge(&rj, &nonce_pts[1]);
secp256k1_ecmult(&rj, &rj, &b0b1, NULL);
secp256k1_gej_add_ge_var(&rj, &rj, &nonce_pts[0], NULL);
if (fin_parity) {
secp256k1_gej_neg(&rj, &rj);
}
secp256k1_scalar_set_b32(&s, &partial_sig->data[5], NULL);
secp256k1_scalar_negate(&s, &s);
secp256k1_gej_set_ge(&dj, &d);
secp256k1_ecmult(&tmp, &dj, &key_coef, &s);
secp256k1_gej_add_var(&tmp, &tmp, &rj, NULL);
return secp256k1_gej_is_infinity(&tmp);
}
int secp256k1_iceberg_partial_sig_agg(const secp256k1_context *ctx, secp256k1_musig_partial_sig *musig_partial_sig, const secp256k1_iceberg_partial_sig * const *partial_sigs, size_t n_partial_sigs, unsigned int n, unsigned int t) {
unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_scalar vals[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_scalar s;
size_t i, j;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(musig_partial_sig != NULL);
memset(musig_partial_sig, 0, sizeof(*musig_partial_sig));
ARG_CHECK(partial_sigs != NULL);
ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
ARG_CHECK(t >= 1 && t <= (n + 1) / 2);
/* Interpolation needs t points. The larger quorum belongs to the nonce
* round, where a degree check has to be sound; here it would only cost
* availability. How many members answered is a fact about the group rather
* than a caller bug, so it returns. The upper bound duplicates what the
* repeated-index check below would catch, and is here because idx and vals
* are sized for the largest group. */
if (n_partial_sigs < t || n_partial_sigs > n) {
return 0;
}
for (i = 0; i < n_partial_sigs; i++) {
ARG_CHECK(partial_sigs[i] != NULL);
}
for (i = 0; i < n_partial_sigs; i++) {
ARG_CHECK(secp256k1_memcmp_var(partial_sigs[i]->data, secp256k1_iceberg_psig_magic, 4) == 0);
idx[i] = partial_sigs[i]->data[4];
if (idx[i] < 1 || idx[i] > n) {
return 0;
}
for (j = 0; j < i; j++) {
if (idx[j] == idx[i]) {
return 0;
}
}
secp256k1_scalar_set_b32(&vals[i], &partial_sigs[i]->data[5], NULL);
}
/* The shares of one session lie on a degree t-1 polynomial, as the nonce round's contributions do, though the
* shares are a linear combination of three degree t-1 sharings with
* group-level coefficients and so lie on a degree t-1 polynomial too. It
* only bites above the threshold: at exactly t shares the interpolation is
* determined and there is nothing to disagree with, which is why passing one
* more share can turn a success into a refusal. A caller with a spare share
* learns that the set contradicts itself here,
* rather than from a signature that fails without naming a share.
*
* This says the set is self-consistent, not that the shares are the ones the
* members would have produced; secp256k1_iceberg_partial_sig_verify answers
* that, one share at a time, against the public share it names. */
if (!secp256k1_scalarpoly_on_degree_var(idx, vals, n_partial_sigs, t)) {
return 0;
}
secp256k1_scalarpoly_interpolate_at0(&s, idx, vals, n_partial_sigs);
secp256k1_musig_partial_sig_save(musig_partial_sig, &s);
return 1;
}
#endif /* SECP256K1_MODULE_ICEBERG_SESSION_IMPL_H */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_VPSS_H
#define SECP256K1_MODULE_ICEBERG_VPSS_H
#include "../../../include/secp256k1.h"
#include "../../group.h"
#include "../../scalar.h"
/* Verifiable pseudorandom secret sharing: the part that runs in the group.
*
* No secret enters this file. Everything here operates on published commitments
* and public participant indices, so there is nothing to leak and every routine
* is free to be variable time.
*
* Two operations, both consequences of the fact that Lagrange interpolation
* uses nothing but addition and multiplication by known scalars, and therefore
* survives being carried into the group:
*
* verify - honest shares are evaluations of one degree t-1 polynomial, so
* interpolating the published commitments must yield zero for every
* coefficient above x^(t-1). A participant who publishes anything
* else raises the degree and is caught.
*
* combine - the same interpolation, evaluated at zero, gives the commitment
* to the shared secret. Any valid quorum produces the same point. */
/* 1 if the m commitments are consistent with a polynomial of degree at most
* t-1, 0 otherwise, with two edges the mathematics does not have. Below t this
* returns 0, although a set that small always lies on such a polynomial: there
* is nothing to test, and a degree check is the wrong place to fail open. At
* exactly t it returns 1 without testing anything, because t points fix the
* polynomial and leave no high coefficient over. Only m > t proves something.
*
* idx holds m distinct participant indices and points their commitments, which
* may include the point at infinity. Soundness needs m >= 2t-1, so that at least
* t of the points are honest and pin the true polynomial. Enforcing it is the
* caller's job; this routine checks the degree and nothing else.
*
* On the paths that reach the transcript the points are normalized in place,
* following the convention of the serialization helpers this shares with the
* musig module; the two early returns above leave them alone. */
static int secp256k1_vpss_verify_var(const secp256k1_context *ctx, const unsigned char *idx, secp256k1_ge *points, size_t m, unsigned int t);
/* The interpolated commitment polynomial, evaluated at `at`.
*
* At zero this is the group's aggregate, which is what aggregation wants. At a
* participant's own index it is what that participant's contribution must have
* been, which is how a signer checks that a set of contributions it was handed
* belongs to the session it thinks it is in, without having to be one of the
* contributors, since it may have been offline when they were produced. */
static int secp256k1_vpss_eval_at_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m, unsigned int at);
/* r <- sum_j lambda_j * points[j], the same interpolation evaluated at zero:
* the commitment to the shared secret.
*
* Returns 1. The int is the return of secp256k1_ecmult_multi_var underneath,
* which with the current implementation can only fail on a callback that fails,
* and the callback here cannot. Callers check it anyway, as the musig module
* does at the same call for the same reason: the day somebody hands that
* multiexponentiation a scratch space, it can fail. */
static int secp256k1_vpss_combine_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m);
#endif /* SECP256K1_MODULE_ICEBERG_VPSS_H */

View File

@@ -0,0 +1,161 @@
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_ICEBERG_VPSS_IMPL_H
#define SECP256K1_MODULE_ICEBERG_VPSS_IMPL_H
#include "../../../include/secp256k1_iceberg.h"
#include "vpss.h"
#include "scalar_poly_impl.h"
#include "../../ecmult.h"
#include "../../group.h"
#include "../../hash.h"
#include "../../scalar.h"
#include "../../util.h"
/* Both operations here are a weighted sum of the same points, so they share one
* callback and differ only in how the weights were computed. */
typedef struct {
const secp256k1_ge *points;
const secp256k1_scalar *weights;
} secp256k1_vpss_multi_data;
static int secp256k1_vpss_multi_callback(secp256k1_scalar *sc, secp256k1_ge *pt, size_t idx, void *data) {
secp256k1_vpss_multi_data *ctx = (secp256k1_vpss_multi_data *)data;
*sc = ctx->weights[idx];
*pt = ctx->points[idx];
return 1;
}
static int secp256k1_vpss_weighted_sum_var(const secp256k1_context *ctx, secp256k1_gej *r, const secp256k1_ge *points, const secp256k1_scalar *weights, size_t m) {
secp256k1_vpss_multi_data data;
data.points = points;
data.weights = weights;
/* No scratch space: the library does not allocate at runtime, and with at
* most ten points the simple path this falls back to is the right one
* anyway. This mirrors what musig's key aggregation does.
*
* Which also means this cannot return 0. Without a scratch space
* ecmult_multi_var takes the simple path, and that fails only on a callback
* that fails; ours cannot. Reaching a zero here would take a change to one
* of those two things, which is why the callers still test it. */
return secp256k1_ecmult_multi_var(&ctx->error_callback, NULL, r, NULL,
secp256k1_vpss_multi_callback, &data, m);
}
/* Initializes SHA256 with fixed midstate. This midstate was computed by applying
* SHA256 to SHA256("Iceberg/batchcoef")||SHA256("Iceberg/batchcoef"). */
static void secp256k1_vpss_batchcoef_sha256_tagged(secp256k1_sha256 *sha) {
static const uint32_t midstate[8] = {
0xdbf8f1f6ul, 0xc46235d4ul, 0xc3d5e6fdul, 0xaed98a69ul,
0x739fc2e8ul, 0x686b55faul, 0xb3b06820ul, 0x7f3c361bul
};
secp256k1_sha256_initialize_midstate(sha, 64, midstate);
}
static int secp256k1_vpss_verify_var(const secp256k1_context *ctx, const unsigned char *idx, secp256k1_ge *points, size_t m, unsigned int t) {
secp256k1_scalar basis[SECP256K1_ICEBERG_MAX_PARTICIPANTS * SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_sha256 transcript;
secp256k1_gej sum;
unsigned char header[2];
unsigned char buf[33];
size_t i, j;
VERIFY_CHECK(t >= 1 && m <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
/* Below t the loop at the end runs zero times, and an empty sum is the
* identity, so this would report success on a set too small to constrain
* anything. No caller reaches it today; a degree check is the wrong place to
* fail open if one ever does. */
if (m < t) {
return 0;
}
/* With m == t the interpolation is exactly determined and there is no
* high coefficient left to test. Nothing has been proved, but nothing has
* been violated either. */
if (m == t) {
return 1;
}
/* The naive check tests each high coefficient separately: for every
* i in [t, m), sum_j basis[j][i] * points[j] must be the identity. Testing
* a random linear combination of those equations instead collapses m-t
* multiexponentiations into one. If any coefficient is non-zero, the
* combination is the identity only if the weights happen to lie on a
* hyperplane, which a hash commits them away from. The transcript covers
* every input that defines the statement, so weights are fixed only after
* the prover has committed to the points. */
secp256k1_vpss_batchcoef_sha256_tagged(&transcript);
header[0] = (unsigned char)t;
header[1] = (unsigned char)m;
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &transcript, header, sizeof(header));
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &transcript, idx, m);
for (j = 0; j < m; j++) {
secp256k1_musig_ge_serialize_ext(buf, &points[j]);
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &transcript, buf, sizeof(buf));
}
secp256k1_scalarpoly_lagrange_basis_var(basis, idx, m);
for (j = 0; j < m; j++) {
secp256k1_scalar_set_int(&weights[j], 0);
}
for (i = t; i < m; i++) {
secp256k1_sha256 fork = transcript;
secp256k1_scalar rho, term;
unsigned char which = (unsigned char)i;
unsigned char out[32];
secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &fork, &which, 1);
secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &fork, out);
secp256k1_scalar_set_b32(&rho, out, NULL);
for (j = 0; j < m; j++) {
secp256k1_scalar_mul(&term, &rho, &basis[j * m + i]);
secp256k1_scalar_add(&weights[j], &weights[j], &term);
}
}
if (!secp256k1_vpss_weighted_sum_var(ctx, &sum, points, weights, m)) {
return 0;
}
return secp256k1_gej_is_infinity(&sum);
}
static int secp256k1_vpss_eval_at_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m, unsigned int at) {
secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
secp256k1_scalar denominators[SECP256K1_ICEBERG_MAX_PARTICIPANTS];
size_t j;
VERIFY_CHECK(m >= 1 && m <= SECP256K1_ICEBERG_MAX_PARTICIPANTS);
/* Fill the tail as well: only the first m entries are read, but GCC cannot
* see that and warns on the partially filled array. */
for (j = 0; j < SECP256K1_ICEBERG_MAX_PARTICIPANTS; j++) {
secp256k1_scalar_set_int(&denominators[j], 1);
}
/* One inversion for the whole set instead of one per weight, as in
* secp256k1_rss_lagrange_weights_var. Both partial_sign and
* partial_sig_verify come through here once per nonce sharing, so twice
* apiece. */
for (j = 0; j < m; j++) {
secp256k1_scalarpoly_lagrange_parts_var(&weights[j], &denominators[j],
idx, m, idx[j], at);
}
secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, m);
for (j = 0; j < m; j++) {
secp256k1_scalar_mul(&weights[j], &weights[j], &denominators[j]);
}
return secp256k1_vpss_weighted_sum_var(ctx, r, points, weights, m);
}
static int secp256k1_vpss_combine_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m) {
return secp256k1_vpss_eval_at_var(ctx, r, idx, points, m, 0);
}
#endif /* SECP256K1_MODULE_ICEBERG_VPSS_IMPL_H */