From e581abad00cd2841815dff53c267388abf6df159 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 31 Aug 2026 12:24:48 +0200 Subject: [PATCH 1/6] 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. --- CMakeLists.txt | 2 + Makefile.am | 4 + configure.ac | 18 + doc/iceberg.md | 441 ++++ include/secp256k1_iceberg.h | 748 +++++++ include/secp256k1_iceberg_dealer.h | 57 + src/CMakeLists.txt | 13 + src/modules/iceberg/Makefile.am.include | 16 + src/modules/iceberg/bench_impl.h | 221 ++ src/modules/iceberg/keygen_impl.h | 422 ++++ src/modules/iceberg/main_impl.h | 22 + src/modules/iceberg/rss.h | 141 ++ src/modules/iceberg/rss_impl.h | 181 ++ src/modules/iceberg/scalar_poly.h | 89 + src/modules/iceberg/scalar_poly_impl.h | 223 +++ src/modules/iceberg/session_impl.h | 842 ++++++++ src/modules/iceberg/tests_impl.h | 2432 ++++++++++++++++++++++ src/modules/iceberg/vectors.h | 2446 +++++++++++++++++++++++ src/modules/iceberg/vpss.h | 67 + src/modules/iceberg/vpss_impl.h | 161 ++ src/secp256k1.c | 4 + src/tests.c | 7 + 22 files changed, 8557 insertions(+) create mode 100644 doc/iceberg.md create mode 100644 include/secp256k1_iceberg.h create mode 100644 include/secp256k1_iceberg_dealer.h create mode 100644 src/modules/iceberg/Makefile.am.include create mode 100644 src/modules/iceberg/bench_impl.h create mode 100644 src/modules/iceberg/keygen_impl.h create mode 100644 src/modules/iceberg/main_impl.h create mode 100644 src/modules/iceberg/rss.h create mode 100644 src/modules/iceberg/rss_impl.h create mode 100644 src/modules/iceberg/scalar_poly.h create mode 100644 src/modules/iceberg/scalar_poly_impl.h create mode 100644 src/modules/iceberg/session_impl.h create mode 100644 src/modules/iceberg/tests_impl.h create mode 100644 src/modules/iceberg/vectors.h create mode 100644 src/modules/iceberg/vpss.h create mode 100644 src/modules/iceberg/vpss_impl.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 21378d99..f1fbb6e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ option(SECP256K1_ENABLE_MODULE_SCHNORRSIG "Enable schnorrsig module." ON) option(SECP256K1_ENABLE_MODULE_MUSIG "Enable musig module." ON) option(SECP256K1_ENABLE_MODULE_FROST "Enable FROST module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_CHILLDKG "Enable ChillDKG module (experimental)." OFF) +option(SECP256K1_ENABLE_MODULE_ICEBERG "Enable Iceberg threshold-MuSig module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_ELLSWIFT "Enable ElligatorSwift module." ON) option(SECP256K1_ENABLE_MODULE_GENERATOR "Enable NUMS generator module." ON) @@ -301,6 +302,7 @@ message(" schnorrsig .......................... ${SECP256K1_ENABLE_MODULE_SCHNO message(" musig ............................... ${SECP256K1_ENABLE_MODULE_MUSIG}") message(" frost ............................... ${SECP256K1_ENABLE_MODULE_FROST}") message(" chilldkg ............................ ${SECP256K1_ENABLE_MODULE_CHILLDKG}") +message(" iceberg ............................. ${SECP256K1_ENABLE_MODULE_ICEBERG}") message(" ElligatorSwift ...................... ${SECP256K1_ENABLE_MODULE_ELLSWIFT}") message(" generator ........................... ${SECP256K1_ENABLE_MODULE_GENERATOR}") message(" rangeproof .......................... ${SECP256K1_ENABLE_MODULE_RANGEPROOF}") diff --git a/Makefile.am b/Makefile.am index 9437e648..d07b3abb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -378,3 +378,7 @@ endif if ENABLE_MODULE_CHILLDKG include src/modules/chilldkg/Makefile.am.include endif + +if ENABLE_MODULE_ICEBERG +include src/modules/iceberg/Makefile.am.include +endif diff --git a/configure.ac b/configure.ac index 4e2f70c1..6dec492e 100644 --- a/configure.ac +++ b/configure.ac @@ -250,6 +250,11 @@ AC_ARG_ENABLE(module_chilldkg, [], [SECP_SET_DEFAULT([enable_module_chilldkg], [no], [yes])]) +AC_ARG_ENABLE(module_iceberg, + AS_HELP_STRING([--enable-module-iceberg],[enable Iceberg threshold-MuSig module (experimental)]), + [], + [SECP_SET_DEFAULT([enable_module_iceberg], [no], [yes])]) + # Test-only override of the (autodetected by the C code) "widemul" setting. # Legal values are: # * int64 (for [u]int64_t), @@ -568,6 +573,14 @@ if test x"$enable_module_chilldkg" = x"yes"; then enable_module_ecdh=yes fi +if test x"$enable_module_iceberg" = x"yes"; then + if test x"$enable_module_musig" = x"no"; then + AC_MSG_ERROR([Module dependency error: You have disabled the musig module explicitly, but it is required by the iceberg module.]) + fi + enable_module_musig=yes + SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_ICEBERG=1" +fi + if test x"$enable_external_default_callbacks" = x"yes"; then SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DUSE_EXTERNAL_DEFAULT_CALLBACKS=1" fi @@ -618,6 +631,9 @@ if test x"$enable_experimental" = x"no"; then if test x"$enable_module_chilldkg" = x"yes"; then AC_MSG_ERROR([ChillDKG module is experimental. Use --enable-experimental to allow.]) fi + if test x"$enable_module_iceberg" = x"yes"; then + AC_MSG_ERROR([Iceberg module is experimental. Use --enable-experimental to allow.]) + fi fi # Check for concurrency support (tests only) @@ -657,6 +673,7 @@ AM_CONDITIONAL([ENABLE_MODULE_BPPP], [test x"$enable_module_bppp" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_SCHNORRSIG_HALFAGG], [test x"$enable_module_schnorrsig_halfagg" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_FROST], [test x"$enable_module_frost" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_CHILLDKG], [test x"$enable_module_chilldkg" = x"yes"]) +AM_CONDITIONAL([ENABLE_MODULE_ICEBERG], [test x"$enable_module_iceberg" = x"yes"]) AM_CONDITIONAL([USE_REDUCED_SURJECTION_PROOF_SIZE], [test x"$use_reduced_surjection_proof_size" = x"yes"]) AM_CONDITIONAL([USE_EXTERNAL_ASM], [test x"$enable_external_asm" = x"yes"]) AM_CONDITIONAL([USE_ASM_ARM], [test x"$set_asm" = x"arm32"]) @@ -700,6 +717,7 @@ echo " module bppp = $enable_module_bppp" echo " module schnorrsig-halfagg = $enable_module_schnorrsig_halfagg" echo " module frost = $enable_module_frost" echo " module chilldkg = $enable_module_chilldkg" +echo " module iceberg = $enable_module_iceberg" echo echo " asm = $set_asm" echo " ecmult window size = $set_ecmult_window" diff --git a/doc/iceberg.md b/doc/iceberg.md new file mode 100644 index 00000000..ae7fed54 --- /dev/null +++ b/doc/iceberg.md @@ -0,0 +1,441 @@ +Notes on the iceberg module API +=============================== + +The following sections contain additional notes on the API of the iceberg +module (`include/secp256k1_iceberg.h`). A usage example can be found in +`examples/iceberg.c`, which runs the whole flow and narrates it. + +**This module is experimental.** It builds by default here, which is a +development convenience rather than a statement that it is ready. Iceberg has a +security proof, by reduction to NestedMuSig2's unforgeability, but it is in an +anonymous conference submission that is still a working draft, and at the two +nonces BIP-327 fixes that reduction holds in the algebraic group model rather +than the plain random oracle model. The proof also assumes a property no library +can provide (that a session label is used once, group-wide) and the known +ways to lose a key all live in exactly that assumption. Do not put money behind +this module. + +Iceberg lets a *t*-of-*n* group act as a single MuSig2 participant. From outside, +the result is an ordinary BIP-340 signature: nothing in it records that a group +was involved, or how large the group was. + +## The objects + +Two APIs are in play and about a dozen nouns between them. MuSig2's, which this +module does not replace: + +| name | type | made by | secret? | size | +|---|---|---|---|---| +| key aggregation cache | `musig_keyagg_cache` | anyone, from the signers' public keys | no | opaque | +| secret nonce | `musig_secnonce` | a signer, once per session | **yes, and it must survive between the rounds** | never serialized | +| public nonce | `musig_pubnonce` | a signer | no | 66 B | +| aggregate nonce | `musig_aggnonce` | anyone | no | 66 B | +| partial signature | `musig_partial_sig` | a signer | no | 32 B | + +And Iceberg's, which exist entirely inside the group: + +| name | type | made by | secret? | size | +|---|---|---|---|---| +| share | `iceberg_share` | the dealer, once | **yes, and it is the only _secret_ anyone stores** | 4 + 32*C(n-1, t-1) B | +| share cache | `iceberg_share_cache` | a participant | no: Lagrange weights, which depend only on which participant you are | opaque | +| public share | `iceberg_pubshare` | a participant | no | 34 B | +| nonce contribution | `iceberg_pubnonce` | a participant, per session | no | 67 B | +| group nonce | `iceberg_aggnonce` | nobody has to: no call takes one as input, so pass `NULL` unless you want it for logging | no | 66 B | +| signature share | `iceberg_partial_sig` | a participant, per session | no | 33 B | + +Two names are close and the objects are not. The group produces a MuSig2 **partial signature**, built +out of Iceberg **signature shares**, one per participant. Likewise a participant +makes a **nonce contribution**, and the group turns `2t-1` of them into one +ordinary MuSig2 **public nonce**. + +Those two conversions are the seam. `iceberg_nonce_agg` emits a +`musig_pubnonce` and `iceberg_partial_sig_agg` emits a `musig_partial_sig`; +everything above them is ordinary MuSig2 that knows nothing about a group, and +everything below is this module. + +## Three roles + +Three roles appear here, and they run different code: + +| role | runs | trusted? | +|---|---|---| +| **participant** | `iceberg_*` calls. Holds a share, never the key. There are *n* of them | with its own share only | +| **coordinator** | moves messages, calls the `_agg` functions | **no.** Every check in the module assumes it is hostile | +| **cosigner** | plain `musig_*` calls, and knows nothing about the group | as any MuSig2 signer | + +A participant may also act as the coordinator; nothing changes if it does, +because the coordinator has no privileges to abuse. + +## How many people, and when + +Two different counts: + +- **Round one needs `2t-1` participants.** Not because the secret needs that + many, but because the group verifies its own nonce contributions, and that + check is error detection. + + A contribution is a point on a polynomial of degree `t-1`, and **any `t` + points lie on some polynomial of that degree**, including `t` an adversary + chose. So a quorum of `t` does not weaken the check, it empties it. Every + point past `t` is one more constraint a liar has to satisfy, and outnumbering + `t-1` liars takes `t-1` of them: + + t + (t-1) = 2t-1 + + Read as coding theory it is the same statement: detecting `e` errors in a code + of dimension `t` needs `t+e` symbols. The `3t-2` under "Not implemented" is + this formula with correction, `t+2e`, in place of detection. Note that the + `n >= 3t-2` deployment bound below is a *different* result that happens to be + the same number: one is Reed-Solomon correction, the other is Byzantine + agreement, and neither implies the other. +- **Round two needs `2t-1` too**, and they need not be the ones who took part in + round one. The paper's Table II gives the signing quorum as `2t-1` online + members for every threshold, and the honest majority that number represents + does not stop applying halfway through a session. + + A member that was offline for round one can still take part: it holds the + share that determines what its contribution would have been, so it can verify + the set it is handed and sign against it. That is the property the + deterministic nonces exist to buy, and it is why `partial_sign` does not + insist on finding your own contribution in the set; see "API misuse". + + `t` appears in round two only as the interpolation degree; the arithmetic + that turns signature shares back into one signature needs `t` points. Reading + that as "round two needs `t` people" is the mistake to avoid. + +Since `2t-1` participants must exist, `2t-1 <= n`, so: + + t <= (n+1)/2 + +This is a hard structural limit, not a performance note. **2-of-2, 3-of-3, +3-of-4, 4-of-5, 4-of-6 and 6-of-10 cannot be expressed at all.** +`secp256k1_iceberg_shares_gen` refuses them at setup rather than mishandling +them later; see the failure table under "API misuse" for what refusing means. + +Sizes, since a share is a bundle of seeds and grows quickly. Both rounds want +`2t-1` members online; the third column is the smaller number the *arithmetic* of +step 10 needs, which is a degree and not a quorum. + +**These are expressible configurations, not recommended ones.** A separate bound +applies to deployment: agreeing on the live state with up to `t-1` faulty members +is Byzantine agreement, which needs `n >= 3(t-1)+1 = 3t-2`. That gives 2-of-4, +3-of-7, 4-of-10, 5-of-13 as the smallest deployable groups, and **five of the +seven rows below fall short of it**. The signature scheme is correct at all of +them, since the arithmetic does not know how many faults the surrounding +agreement survives, but a group sized from this table alone will be too small for the +consensus the scheme assumes. See the deployment constraints in +`include/secp256k1_iceberg.h`. + +Note where the two bounds meet. 5-of-13 needs more participants than +`SECP256K1_ICEBERG_MAX_PARTICIPANTS` allows, so at the maximum of 10 the +largest threshold that is both expressible and deployable is **4-of-10**. The +table below goes past that line, and so do the benchmark and the tests: 5-of-9 +and 5-of-10 are there to show the cost curve, not because a group should be +sized that way. + +`SECP256K1_ICEBERG_MAX_PARTICIPANTS` is 10. It sizes two of the opaque types and +the largest stack frames in the module, all of which grow as `C(n-1, t-1)`, so it +is part of the ABI and not something one member of a group changes on its own. A +build that will only ever run small groups can lower it, and gets a 200-byte share +instead of a 4040-byte one at five participants; `src/modules/iceberg/rss.h` +beside the `#error` says which three derived values have to be lowered with it, +and `run_iceberg_binom_test` recomputes all three and names the one that is wrong. +Raising it is refused at compile time. `rss.h` also carries the command that +measures the stack frames, rather than a number, because the number moves with the +compiler. None of that is in the public header: a caller of the installed library +has no `rss.h`, no test suite and no `src/` to run it against. + +| config | quorum `2t-1` | shares that interpolate | seeds in the group | seeds per participant | serialized share | +|---|---|---|---|---|---| +| 2-of-3 | 3 | 2 | 3 | 2 | 68 B | +| 2-of-4 | 3 | 2 | 4 | 3 | 100 B | +| 3-of-5 | 5 | 3 | 10 | 6 | 196 B | +| 3-of-7 | 5 | 3 | 21 | 15 | 484 B | +| 4-of-7 | 7 | 4 | 35 | 20 | 644 B | +| 5-of-9 | 9 | 5 | 126 | 70 | 2244 B | +| 5-of-10 | 9 | 5 | 210 | 126 | 4036 B | + +Everything that crosses the network serializes: a share to `4 + 32*C(n-1, t-1)` +bytes, a public share to 34, a nonce contribution to 67 and a signature share to +33. The group's aggregate nonce serializes to 66 as well, but it is not on that +list: nothing receives one. Those bytes are the start of the b1 preimage, and +they are there for an implementation checking its arithmetic against this one. + +The public share, the nonce contribution and the signature share each carry the +participant index they belong to, which is the extra byte in each. The group's +aggregate nonce does not, since it belongs to the group rather than to a member, +which is exactly why it is 66 bytes and a contribution is 67. + +The group's seed count is `C(n, t-1)` and each participant holds +`C(n-1, t-1)` of them. The bounds derived from +`SECP256K1_ICEBERG_MAX_PARTICIPANTS` (`SECP256K1_ICEBERG_MAX_SEEDS` here, and +`MAX_T` and `MAX_SUBSETS` in `rss.h`) are written out by hand because C89 +cannot evaluate a binomial at preprocessing time. The test suite recomputes each +of them and fails on a wrong line. + +## The flow + + participant k coordinator cosigner + (one of n) (untrusted) (plain musig2) + ------------- ----------- -------------- + + SETUP, once. A dealer runs shares_gen and is then not needed again. + iceberg_shares_gen -> one share each [_dealer.h, not installed] + iceberg_pubshare_gen -> iceberg_pubkey_agg -> the group's public key + + KEY AGGREGATION. The group is now one public key among several. + musig_pubkey_agg([group_pk, cosigner_pk, ...]) -> keyagg_cache + optionally musig_pubkey_xonly_tweak_add / _ec_tweak_add + + === ROUND ONE ================================ needs 2t-1 participants === + + Neither side waits on the other. iceberg_nonce_gen needs only the share + and the label; musig_nonce_gen is an ordinary MuSig2 call and takes + whatever it usually takes. Drawn side by side for that reason. + + iceberg_nonce_gen(share, sid32) musig_nonce_gen + --- pubnonce (67 B) --> + <-- pubnonce ------ + iceberg_nonce_agg + verifies 2t-1 of them, interpolates + -> one ordinary musig pubnonce + musig_nonce_agg([group, cosigners]) + --- aggnonce --> + musig_nonce_process + + ------------------------------------------------------------------------ + THE GAP. No secret nonce survives this line. A participant's nonces + are a function of (share, sid), and it is handed the sid again, so a + crash here costs it nothing and there is no secret to lose, leak or + duplicate. That is narrower than "stores nothing": it must still + remember which labels it has answered under, and restoring a backup + from before that record is exactly the exploitable case. + ------------------------------------------------------------------------ + + === ROUND TWO ============================ needs 2t-1 again, any of n === + + The message appears here for the first time, which is the point: the + group commits to a nonce before knowing what it will sign. + + --- msg, the round-one pubnonces --> + iceberg_partial_sign(share, sid32, msg, the contributions, ...) + derives the group's aggregate from the contributions rather than + accepting one, checks they are a single sharing of degree t-1, and + compares their value at its own index against the contribution it + derives for itself. So it signs against the aggregate the group + really formed. A member that sat round one out can still sign. + --- partial sig (33 B) --> + iceberg_partial_sig_agg + -> one ordinary musig partial sig + musig_partial_sign + <-- partial sig --- + musig_partial_sig_agg -> 64-byte signature + + schnorrsig_verify accepts it under the (possibly tweaked) aggregate key. + +## Signing, as a list + +1. **Dealer**: `secp256k1_iceberg_shares_gen`, from + `include/secp256k1_iceberg_dealer.h`, then hand each participant its share + and forget the seed. That header is separate and is not installed, because a + trusted dealer is not something the library offers for deployment; see + "API misuse" below. A distributed key generation replaces this step and + nothing downstream changes. +2. **Each participant**: `secp256k1_iceberg_pubshare_gen`, publish the result. +3. **Anyone**: `secp256k1_iceberg_pubkey_agg` over any `2t-1` public shares. + This checks they agree, so a participant that published a wrong one is + caught here rather than at signing time. +4. **Anyone**: `secp256k1_musig_pubkey_agg` with the group's public key and the + cosigners', then optionally `secp256k1_musig_pubkey_xonly_tweak_add` and + `secp256k1_musig_pubkey_ec_tweak_add`. +5. **Each of `2t-1` participants**: `secp256k1_iceberg_nonce_gen`, over the + session label alone. Publish the result. Nothing here depends on the + message or on the cosigners, so this can run before either exists. +6. **Cosigners**, independently and in either order: `secp256k1_musig_nonce_gen`, + then `secp256k1_musig_nonce_agg` over the cosigners' nonces alone. +7. **Coordinator**: `secp256k1_iceberg_nonce_agg`, which verifies and + interpolates them into one ordinary MuSig2 public nonce. +8. **Coordinator**: `secp256k1_musig_nonce_agg` over that and the cosigners', + then `secp256k1_musig_nonce_process`. +9. **Participants**, not necessarily the ones from step 5: + `secp256k1_iceberg_partial_sign`, given the group's own round-one + contributions, not an aggregate of them and not the cosigners', the same + label, and now the message. This is where the message enters and the only + place it appears in the API. Each call needs all `2t-1` contributions from + step 5 and returns 0 with fewer, and the scheme wants `2t-1` members online + here as in round one; step 10 then interpolates from as few as `t` of the + resulting shares. Before calling it, each signer checks its own record that it + has not answered under this label already; the library cannot do that for it, + because it holds nothing between calls. +10. **Coordinator**: `secp256k1_iceberg_partial_sig_agg`, giving one MuSig2 + partial signature. Hand it more than `t` shares and it degree-checks them, + which above the threshold catches a set that contradicts itself; at exactly + `t` there is nothing to disagree with. +11. **Cosigners**: `secp256k1_musig_partial_sign` as usual. +12. **Coordinator**: `secp256k1_musig_partial_sig_agg`, then + `secp256k1_schnorrsig_verify`. + +Steps 1 to 4 happen once per group, and `secp256k1_iceberg_keyagg_check` belongs +with step 4: it confirms the outer cache aggregates the key list you think it +does, which is a fact about the channel rather than about this attempt. Steps 5 +to 12 are one signing session. + +## API misuse + +The musig module's three rules apply here too: unique nonces, never copy or +serialize a `secp256k1_musig_secnonce`, and never read or write an opaque struct +directly. Iceberg adds its own, and the reasons are specific. + +**How a call refuses, before anything else.** A function returns 0 when the +values it was handed do not work together: an inconsistent set of shares, a +malformed encoding, a contribution derived under some other label. The illegal +callback, which aborts the process unless the caller has installed its own with +`secp256k1_context_set_illegal_callback`, is reserved for a bug in the calling +code: a null pointer, an uninitialized opaque struct, a group shape the scheme +cannot express. + +How many contributions turned up is neither. It is a fact about the group, +influenced by whichever peers answered, so every call that takes a count returns +0 rather than aborting: + +| call | a bad count does what | +|---|---| +| `iceberg_shares_gen` | aborts: `n` outside `1..10`, or `t` outside `1..(n+1)/2`. These are the group's shape, not a count | +| `iceberg_pubkey_agg` | returns 0: fewer than `2t-1` public shares, or more than `n` | +| `iceberg_nonce_agg` | returns 0: fewer than `2t-1` contributions, or more than `n` | +| `iceberg_partial_sig_agg` | returns 0: fewer than `t` shares or more than `n`. A share that never came from `partial_sign` still aborts, that one being an uninitialized struct | +| `iceberg_partial_sign` | returns 0: it checks the same `2t-1` bound while deriving the aggregate | +| `iceberg_partial_sig_verify` | returns 0: same reason, and it derives the same aggregate | + +A member that waits for a deadline and aggregates whatever arrived is therefore +doing something the API supports, rather than something that kills its process +the first time a peer is asleep. + +**Where n and t come from.** `iceberg_partial_sign` reads them off the caller's +own share and never asks. The four calls that take them as arguments should be +given the same values; a member that keeps only a serialized share can read them +from it, since the encoding is `version | n | t | index | seeds`. It matters most +for `t`: understate it and the degree check still runs, against a lower degree, +and proves less. Nothing cross-checks the four calls against each other. + +**You supply the session label, and the rule about it is yours to enforce.** Every participant's secret nonces are a +deterministic function of its seeds and the label, and the seeds never change, +so a label reused under two different messages produces two answers in which +`k1`, `k2` and `d` are identical while `b0` and `e` have moved. Three such +answers are three linear equations in those three unknowns, and solving them +recovers a key share. + +Concretely, the caller must guarantee both halves: + +- **one answer per member per label.** A member can enforce this alone, but not + from anything the library holds; it keeps nothing between calls. It needs + durable storage of its own: one 32-byte field per participant holding the last + label it signed under, and a rule that a new label must be strictly greater. + `may_sign_under` in `examples/iceberg.c` does exactly that. + Restore it with the share; a record rolled back to an old backup is a member + that will answer twice. +- **one message per label across the whole group**: the members must agree on + what they are signing before any of them answers. + +The library sees one call at a time and holds nothing between them, so it can +enforce neither. Neither can any +individual participant: a coordinator can show three *different* members three +*different* messages under one label, and each one signs exactly once, refuses +nothing, and detects nothing, because nothing in the protocol tells a member +that somebody else saw this label too. **Per-participant discipline is therefore +not a substitute for group agreement**, which is why the scheme's security model +assumes a consensus its deployment already runs. + +*Why the label cannot simply be derived from the message, which would close all +of this:* round one has to run before the message exists. In Lightning the +nonce is exchanged a full round-trip before the commitment transaction is +assembled, so a label binding the message could not be computed when it is +needed. The intended label is the channel's commitment number, plus a counter +for retries under it. A label bound to the message is possible where the message +*is* known early, but it costs the message-independent first round, which is a different setting from the one this +module assumes. The module takes any 32 bytes and offers no derivation, because +the right label depends on the deployment, and the wrong one costs a key share. + +**`secp256k1_iceberg_partial_sign` takes the group's own round-one +contributions, not an aggregate of them.** This is not an optimisation +opportunity. The nesting coefficient is a hash of the group's aggregate nonce, +so a coordinator free to invent that aggregate gets a coefficient it can vary at +will: three invented aggregates under one correctly-bound label again give +three equations in the same three unknowns. The aggregate is therefore derived +from contributions that must pass the degree check together, and then the +interpolated polynomial is evaluated at the signer's own index to get the nonce +shares it signs with. It signs against the aggregate the group actually formed, +whether or not it was one of the members who helped form it. + +It does *not* require the signer's own round-one contribution to be present and +unaltered in the set. That would be strictly stronger against an unauthenticated +transport, and it would lock out a member who was offline during round one -- +the exact failure the deterministic nonces exist to survive. With the +authenticated transport the scheme assumes anyway it is unnecessary: among +`2t-1` contributions with at most `t-1` corruptions, at least `t` are honest, +and `t` points already pin a degree `t-1` polynomial. + +**A tweak belongs to the outer session and must not be applied twice.** The +module is correct here and needs nothing from the caller. No Iceberg call takes +a tweak because `secp256k1_musig_nonce_process` sets +the tweak term aside and `secp256k1_musig_partial_sig_agg` adds it in once, at +the top. The group's shares carry the key coefficient and not the tweak. + +**`secp256k1_iceberg_shares_gen` is a trusted dealer, and lives outside the +installed API for that reason.** For the duration of that one call, one machine +holds enough to reconstruct the group's private key, which is the situation a +threshold scheme exists to avoid. It is fine for testing, and fine where one +party is already trusted with the whole key. It is not fine otherwise, and this +module does not provide a distributed key generation. + +It is declared in `include/secp256k1_iceberg_dealer.h`, which is in the tree so +the tests, the benchmarks and the example have shares to work with, and is not +installed. Including `secp256k1_iceberg.h` does not offer you a dealer. Nothing +in the signing API cares how a share was produced: one arrives through +`secp256k1_iceberg_share_parse`, so a share from a DKG or from another +implementation is used identically. + +## What is stored, and for how long + +| object | who holds it | lifetime | secret? | +|---|---|---|---| +| `iceberg_share` | one participant | forever | **yes** | +| `iceberg_share_cache` | one participant | optional, derived from the share | no | +| `iceberg_pubshare` | published | forever | no | +| session label (`sid32`) | anyone | one session, and a record that it was used, for as long as the group lives | no | +| `iceberg_pubnonce` | published | one session | no | +| `iceberg_aggnonce` | coordinator | one session | no | +| `iceberg_partial_sig` | published | one session | no | + +Nothing marked secret above needs to survive a reboot except the share, +which is why `examples/iceberg.c` wipes every participant between the two rounds +and rebuilds them from storage. That is the property Iceberg exists for: a FROST +signer must keep a secret nonce alive across the same gap, and losing it, +restoring an old copy over it, or running two instances of the signer are all +catastrophic. + +It does not follow that a participant is stateless, and the table does not say +so. A member must also remember **which labels it has already answered under**. +That record is not secret and so is not listed above, but it is not optional: restoring a backup taken before it +was written is the one restore that is dangerous. A signer with no memory can +also be talked into signing a superseded channel state, which in Lightning costs +it the channel. + +The share cache holds no secrets despite being derived from a share: it is +Lagrange weights, which depend only on which participant it is. It has no +serialized form and does not need one; passing `NULL` wherever a cache is +accepted rebuilds it. `bench_iceberg` prints what that costs, along with the +rest of the module; the figure moves with the machine. + +## Not implemented + +- **Naming the liar.** `secp256k1_iceberg_partial_sig_verify` ships, so a share + can be checked against the commitments it claims to come from, and a failed + signature need not be a mystery. What it does not do is assign blame: a 0 + means that share does not satisfy the equation, not that its author cheated, + because MuSig2 partial signatures are forgeable. Naming the liar is error + *correction* rather than detection, and correction needs `t+2e` points where + detection needed `t+e`, so `3t-2` online at once, against `2t-1` for + signing. That is usually more people than the group has, which is why the + function is documented as detection and stops there. +- **Distributed key generation.** See above. diff --git a/include/secp256k1_iceberg.h b/include/secp256k1_iceberg.h new file mode 100644 index 00000000..70678f67 --- /dev/null +++ b/include/secp256k1_iceberg.h @@ -0,0 +1,748 @@ +#ifndef SECP256K1_ICEBERG_H +#define SECP256K1_ICEBERG_H + +#include "secp256k1_musig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** This module implements Iceberg, a threshold scheme that lets a group of + * parties stand in for a single MuSig2 (BIP 327) participant. + * + * WARNING: EXPERIMENTAL. Neither the scheme nor this implementation has been + * reviewed by anyone outside the project, and should not be used to protect + * anything of value. + * + * The 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. + * + * The order of calls, once per group and then once per signature: + * + * setup a distributed key generation produces one share each, then + * pubshare_gen and pubkey_agg give the group's public key, + * which goes into musig_pubkey_agg beside the cosigners' + * round 1 each member of the quorum runs nonce_gen; nonce_agg turns + * those into one ordinary musig_pubnonce, and from there + * upwards this is plain MuSig2 + * round 2 each member runs partial_sign, which takes the message, the + * cosigners' nonce and the round-one contributions; + * partial_sig_agg turns the results into one ordinary + * musig_partial_sig + * + * You can find an example demonstrating the iceberg module in + * examples/iceberg.c. Further notes on API usage can be found in + * doc/iceberg.md. + * + * Nonces are derived from a session label, sid32 below, rather than stored, + * so no signer holds a secret nonce between the two rounds. A label is a + * 32-byte value the caller chooses, one per signing attempt; it is public and + * it need not be random, but it must never be used twice. That is narrower + * than keeping no state at all: a member still has to remember which labels it + * has already answered under. Choosing labels is the one thing this API cannot + * do for you, so read the note on secp256k1_iceberg_partial_sign before + * writing any of this. + * + * Three things constrain how this can be used: + * + * 1. Both rounds need a quorum of 2t-1 members online, of whom up to t-1 may + * be corrupt. Since 2t-1 members have to exist, the threshold is at most + * half the group rounded up: 2-of-3 and 3-of-5 can be expressed where + * 2-of-2 and 3-of-4 cannot. Constraint 3 then rules out 2-of-3 and 3-of-5 + * as well, leaving 2-of-4 as the smallest usable group. + * + * 2. Messages between participants must travel over authenticated channels, and + * a contribution must be accepted only from the member its index names. + * + * Every check below works by outnumbering the at most t-1 corrupt members + * with t honest ones, so that count has to be real, and this library cannot + * take it for you. It can tell that an index names a member of the group and + * that no two contributions claim the same one. It cannot tell who sent + * anything, and no check on the values could: the values of both rounds lie + * on a polynomial of degree t-1, so once t of them are public, anyone can + * compute the rest. A contribution that verifies proves the value is right, + * which is a public property, not that the named member produced it. + * + * What that costs if it is skipped: an adversary supplying t of the 2t-1 + * chooses the group's nonce. It picks a polynomial through the t-1 honest + * points it has to match, which leaves a coefficient free, and computes its + * own points on that. Every check here then passes. + * + * 3. The group must agree what it is signing before anyone answers, and + * agreeing while up to t-1 members misbehave needs n >= 3t-2 members to + * exist: 4 for a threshold of 2, 7 for 3, 10 for 4. Nothing here checks + * that, because it is a property of the group rather than of the + * arithmetic. It is still the bound to size a deployment with. + */ + +/** Maximum number of participants in a group. + * + * A participant holds one 32-byte seed per (t-1)-subset it is not a member of, + * so both storage and signing cost grow as C(n-1, t-1): 2 seeds at 2-of-3, 6 at + * 3-of-5, 126 at 5-of-10. + * + * It sizes two of the opaque types below, so it is part of this library's ABI + * and not something one member of a group can change on its own. + */ +#define SECP256K1_ICEBERG_MAX_PARTICIPANTS 10 + +/** Largest number of seeds one participant can hold, C(9, 4): the maximum of + * C(n-1, t-1) over every expressible configuration, since a quorum of 2t-1 has + * to fit in the group and so t is at most (n+1)/2. + * + * This is the number that makes a group expensive: it sizes two of the opaque + * types below, and the module's largest stack frames with them. + */ +#define SECP256K1_ICEBERG_MAX_SEEDS 126 + +/** Largest buffer secp256k1_iceberg_share_serialize can need. A share of a + * particular group serializes to 4 + 32*C(n-1, t-1) bytes, which is smaller + * for every configuration except the largest; use this to size a fixed buffer + * without computing binomials. */ +#define SECP256K1_ICEBERG_SHARE_MAX_LEN (4 + 32 * SECP256K1_ICEBERG_MAX_SEEDS) + +/** Opaque data structures + * + * The layout inside these is implementation defined and not portable between + * platforms or versions. Use the serialization and parsing functions to move + * them between machines or to storage. They contain no pointers, so they can + * be copied and moved. But a copy of a share is another copy of the secret, + * and erasing one means erasing all of them. + * + * A stray pointer or an uninitialized struct is caught rather than used. + * + * Every participant has an index in 1..n, assigned when the group is dealt. It + * travels inside the share and inside every object derived from one, so the + * aggregation functions can tell contributions apart, and a contribution + * separated from its index is not usable. + * + * The index is a claim the object makes about itself. This library checks only + * that it names a member the group has: everything above n is refused, and + * within 1..n nothing here tells a genuine contribution from one an adversary + * wrote. Binding it to a member is constraint 2 above. + */ + +/** Opaque data structure holding a participant's long-term secret share. + * + * This is the material to guard and to back up, and it is not one number: it + * is a collection of 32-byte seeds, one for every group of t-1 participants + * that this participant is NOT a member of. + * + * There is no resharing here, so a compromise or a change of membership means + * dealing the group again from scratch. + * + * Guaranteed to be 4040 bytes in size: sized for the worst case the + * compile-time maximum allows, whatever n and t you actually use. Serializing + * writes only the seeds in use. + */ +typedef struct secp256k1_iceberg_share { + unsigned char data[8 + 32 * SECP256K1_ICEBERG_MAX_SEEDS]; +} secp256k1_iceberg_share; + +/** Opaque data structure holding the Lagrange weights derived from a share. + * + * Despite traveling with the share, this contains NO secret material. The + * weights depend only on participant indices and subset structure, both + * public, so nothing here needs protecting. It exists purely so that signing + * does not repeat the work, and it can be discarded and rebuilt at any time -- + * which is the only way to move one, since it has no serialized form. + */ +typedef struct secp256k1_iceberg_share_cache { + unsigned char data[8 + 32 * SECP256K1_ICEBERG_MAX_SEEDS]; +} secp256k1_iceberg_share_cache; + +/** Opaque data structure holding one participant's public key share. + * + * A commitment to the participant's key share: the point you get by + * multiplying that share by the generator, plus the index it belongs to. It + * reveals nothing about the share and is meant to be published. This is what + * the group aggregates into its public key, and what lets everyone check that + * nobody published a wrong one. + * + * Guaranteed to be 69 bytes in size. Serializes to 34 bytes. + */ +typedef struct secp256k1_iceberg_pubshare { + unsigned char data[69]; +} secp256k1_iceberg_pubshare; + +/** Derive the Lagrange weights for a share. + * + * An optimization, not a requirement. Three calls need these weights -- + * secp256k1_iceberg_pubshare_gen, secp256k1_iceberg_nonce_gen and + * secp256k1_iceberg_partial_sign, and each accepts NULL and recomputes them, + * at the cost of one scalar inversion and multiplications growing as + * C(n-1, t-1). Build one if a participant signs often enough to care. + * + * The weights depend on n, t and the participant index and on nothing else, so + * a cache is checked against those three rather than against the seeds. One + * built for a different participant, threshold or group size calls the illegal + * callback. One built for a different group of the same shape is accepted: it + * holds the same weights the share would have derived. + * + * Returns: 1 on success. A malformed share calls the illegal callback. + * Args: ctx: pointer to a context object + * Out: cache: pointer to a cache object to initialize + * In: share: the share to derive weights for + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_share_cache_create( + const secp256k1_context *ctx, + secp256k1_iceberg_share_cache *cache, + const secp256k1_iceberg_share *share +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a share. Writes 4 + 32*C(n-1, t-1) bytes. + * + * The encoding is a version byte, then n, then t, then this participant's + * index, then the seeds. Only the seeds are secret. The three before them are + * what makes the seeds interpretable (their number is C(n-1, t-1), and which + * subset each belongs to follows from n, t and the index), so a caller that + * needs any of the three reads it here, off its own buffer. + * + * On entry outlen is the size of the buffer; on return it is the number of + * bytes this share needs, whether or not they fitted. So a short buffer costs + * a second call rather than a guess, or size it at + * SECP256K1_ICEBERG_SHARE_MAX_LEN and never ask. A malformed share has no + * length to report and sets outlen to zero, so a caller looping on it stops + * rather than asking for the same buffer forever. + * + * Returns: 1 on success, 0 if the buffer is too small. A malformed share + * calls the illegal callback. + * Args: ctx: pointer to a context object + * Out: out: buffer to write to + * In/Out: outlen: buffer size in, bytes needed out + * In: share: the share to serialize + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_share_serialize( + const secp256k1_context *ctx, + unsigned char *out, + size_t *outlen, + const secp256k1_iceberg_share *share +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Parse a share. + * + * Returns: 1 on success, 0 if the input is malformed. + * Args: ctx: pointer to a context object + * Out: share: the share to initialize + * In: in: serialized share + * inlen: its length + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_share_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_share *share, + const unsigned char *in, + size_t inlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Compute this participant's public key share. + * + * Returns: 1 on success. A malformed share calls the illegal callback. + * Args: ctx: pointer to a context object (not secp256k1_context_static) + * Out: pubshare: the public key share + * In: share: this participant's share + * cache: its Lagrange weights, or NULL to recompute them + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a public key share to 34 bytes. + * + * Returns: 1, or 0 on a malformed public share, which is a caller error and + * also calls the illegal callback. + * Args: ctx: pointer to a context object + * Out: out34: pointer to a 34-byte array to write to + * In: pubshare: the public key share to serialize + */ +SECP256K1_API int secp256k1_iceberg_pubshare_serialize( + const secp256k1_context *ctx, + unsigned char *out34, + const secp256k1_iceberg_pubshare *pubshare +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse a public key share from 34 bytes. + * + * Returns: 1 on success, 0 if the input does not encode a valid share. + * Args: ctx: pointer to a context object + * Out: pubshare: the public key share to initialize + * In: in34: pointer to a 34-byte serialized share + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_pubshare_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_pubshare *pubshare, + const unsigned char *in34 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Verify public key shares and combine them into the group public key. + * + * Checks that the shares lie on a polynomial of degree at most t-1 and then + * interpolates them. A participant who published anything else raises the + * degree and is caught here, though the check proves only that the set is + * inconsistent, not which member is at fault. + * + * Soundness needs at least t honest points present to pin the true polynomial, + * which is where the quorum of 2t-1 comes from: with at most t-1 members + * corrupt, 2t-1 contributions leave t honest ones, and t points fix a + * polynomial of degree t-1. Those t honest points have to be real: this call + * refuses an index above n, and constraint 2 is the caller's half. + * + * The result is an ordinary public key. Pass it to secp256k1_musig_pubkey_agg + * alongside the cosigners' keys exactly as if it belonged to a single signer. + * + * Returns: 1 on success, 0 if there are fewer than 2t-1 shares or more than + * the group size, if the shares are inconsistent, if two carry the + * same index, or in the vanishingly unlikely case that they combine + * to the point at infinity. + * Args: ctx: pointer to a context object + * Out: group_pk: the group's public key + * In: pubshares: array of pointers to public key shares + * n_pubshares: how many + * n: the group size the shares were dealt for. A share + * carrying an index above it is rejected: no member sits + * there, so nothing could have authenticated it + * t: the threshold, at least 1 and at most (n+1)/2. An n or + * a t outside its range is a caller bug and calls the + * illegal callback + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Opaque data structure holding one participant's nonce contribution. + * + * Guaranteed to be 133 bytes in size. Serialized and parsed with + * secp256k1_iceberg_pubnonce_serialize and secp256k1_iceberg_pubnonce_parse. + */ +typedef struct secp256k1_iceberg_pubnonce { + unsigned char data[133]; +} secp256k1_iceberg_pubnonce; + +/** Opaque data structure holding the group's combined nonce. + * + * This is the group's internal nonce pair (R1, R2'), before the nesting + * coefficient is applied. What goes up to the cosigners is an ordinary + * secp256k1_musig_pubnonce instead. + * + * It is here for an implementation checking its own arithmetic, which needs the + * first 66 bytes of the b1 preimage and can get them nowhere else: the published + * pair has already had b1 applied and does not yield them back. The coefficient + * is + * b1 = H_Iceberg/noncecoef(R1 || R2' || P), where P is the group's public key + * and each point uses the 33-byte encoding MuSig2 uses for aggregate nonces. + * The published pair is (R1, b1*R2'): only the second point is scaled, because + * the coefficient enters as b1^(i-1). + * + * No call here takes one as input, so the serializer below is the only one + * that reads it. It is also the only object you can decline to receive: + * secp256k1_iceberg_nonce_agg writes it if you want it and skips it if you + * pass NULL. + * + * Guaranteed to be 132 bytes in size. Serialized and parsed with + * secp256k1_iceberg_aggnonce_serialize and secp256k1_iceberg_aggnonce_parse. + */ +typedef struct secp256k1_iceberg_aggnonce { + unsigned char data[132]; +} secp256k1_iceberg_aggnonce; + +/** Opaque data structure holding one participant's signature share. + * + * Guaranteed to be 37 bytes in size. Serialized and parsed with + * secp256k1_iceberg_partial_sig_serialize and + * secp256k1_iceberg_partial_sig_parse. + */ +typedef struct secp256k1_iceberg_partial_sig { + unsigned char data[37]; +} secp256k1_iceberg_partial_sig; + +/** Serialize and parse the round-one and round-two objects. + * + * pubnonce 67 bytes a member's nonce contribution, index and two points + * aggnonce 66 bytes the group's internal aggregate, no index + * partial_sig 33 bytes a member's signature share, index and one scalar + * + * A contribution and a signature share carry their participant index because + * everything that consumes them is indexed; an aggregate belongs to the group + * rather than to a member and so has none, which is the whole of the 67-versus- + * 66 difference. Points use the 33-byte encoding MuSig2 uses for aggregate + * nonces, all zero for the point at infinity. The encoding has room for it, + * though secp256k1_iceberg_nonce_agg refuses to emit one. + * + * Parsing rejects an index outside 1..SECP256K1_ICEBERG_MAX_PARTICIPANTS, a + * point that is not on the curve, and a signature share that is not a scalar + * below the group order. The parsers do not know the group size, so an index + * of 9 parses in a group of 5 and is caught when something that does know n + * tries to use it. None of these is a security boundary on its own, since a + * well-formed lie passes all of them, but each turns a value that would fail + * later into one that fails here. + * + * Returns: for a parser, 1 on success and 0 if the encoding is invalid, which + * is an ordinary thing for bytes off a network to be. For a + * serializer, 1, or 0 on a malformed object, which is a caller error + * and also calls the illegal callback. + * Args: ctx: pointer to a context object + * Out/In: the object and the fixed-size buffer named in each declaration + */ +SECP256K1_API int secp256k1_iceberg_pubnonce_serialize( + const secp256k1_context *ctx, + unsigned char *out67, + const secp256k1_iceberg_pubnonce *pubnonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_pubnonce_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_pubnonce *pubnonce, + const unsigned char *in67 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API int secp256k1_iceberg_aggnonce_serialize( + const secp256k1_context *ctx, + unsigned char *out66, + const secp256k1_iceberg_aggnonce *aggnonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_aggnonce_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_aggnonce *aggnonce, + const unsigned char *in66 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API int secp256k1_iceberg_partial_sig_serialize( + const secp256k1_context *ctx, + unsigned char *out33, + const secp256k1_iceberg_partial_sig *partial_sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_partial_sig_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_partial_sig *partial_sig, + const unsigned char *in33 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Derive this participant's nonce contribution for a session. + * + * Depends on the share and the session label and nothing else: no message, no + * cosigner nonce, no key aggregation cache. That is what lets this round run + * before the message exists, which is the property the whole scheme is built + * around, and it is why a member absent from this round is not shut out of the + * next one: its contribution is determined, so it can work out what it would + * have been. There is no secret nonce object to hold between the rounds -- + * round two recomputes the value from the share and the same label. + * + * Returns: 1 on success. A malformed share calls the illegal callback. + * Args: ctx: pointer to a context object (not secp256k1_context_static) + * Out: pubnonce: this participant's contribution, to publish + * In: share: this participant's share + * cache: its Lagrange weights, or NULL to recompute them + * sid32: the session label + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(5); + +/** Verify nonce contributions and combine them. + * + * Checks both nonce sharings the way key aggregation checks the key sharing, + * and for the same reason, then interpolates them into one ordinary MuSig2 + * public nonce. From that nonce upwards this is plain MuSig2. + * + * Returns: 1 on success, 0 if there are fewer than 2t-1 contributions or more + * than the group size, if they are inconsistent, if two carry the + * same index, or if the group's nonce comes out at infinity. + * Args: ctx: pointer to a context object + * Out: musig_pubnonce: the group's nonce, to publish to the cosigners + * aggnonce: the group's internal nonce, or NULL if you do not + * want it. Nothing in this API consumes one + * In: pubnonces: the contributions + * n_pubnonces: how many + * n: the group size. A contribution carrying an index + * above it is rejected + * t: the threshold, at least 1 and at most (n+1)/2. An n + * or a t outside its range is a caller bug and calls + * the illegal callback + * group_pk: the group's public key. It feeds the nesting + * coefficient, so round two must be given the same one + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) + SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(8); + +/** Does this key aggregation cache aggregate this list, and is the group in it? + * + * Run this once, where the cache is built, and not once per session: the key + * set belongs to the channel while a session belongs to a single signing + * attempt. + * + * secp256k1_iceberg_partial_sign takes the group's public key and the outer + * cache as two separate arguments and nothing ties them together, because + * nothing can: a cache records the hash of the key list, not the list, so + * membership is not a question it can answer. Passing a cache built over some + * other set of cosigners produces a well-formed coefficient for a key that is + * not in the aggregation, a signature share that is useless, and a spent session + * label, which is the part that matters. Retrying under that label with + * the right cache is then the attack, self-inflicted. + * + * MuSig2's own partial_sign asks for no key list, because there a wrong cache + * costs a nonce rather than a label. + * + * Tweaks are fine. The list hash is fixed when the keys are aggregated and no + * tweak touches it, so a cache carrying taproot tweaks still passes. + * + * Returns: 1 if keyagg_cache aggregates exactly this list of public keys, in + * this order, and group_pk is one of them. 0 if it aggregates some + * other list, or if group_pk is not in it. A malformed cache or public + * key is a caller bug rather than an answer, and calls the illegal + * callback as it does everywhere else in this module: neither has a + * wire format, so neither can have arrived from a peer + * Args: ctx: pointer to a context object + * In: keyagg_cache: the outer MuSig2 key aggregation cache to check + * pubkeys: the keys it should have been built from, in the order + * they were passed to secp256k1_musig_pubkey_agg + * n_pubkeys: how many, at least 1 + * group_pk: the group's public key, which must be one of them + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(5); + +/** Produce this participant's signature share. + * + * A participant makes its own three secrets alone: both nonce scalars and its + * key share follow from its share and the label, with nobody else involved. What + * it cannot make alone is the group's aggregate nonce, which is a value at zero + * and so takes t points, and that aggregate is hashed into two of the + * coefficients those secrets get multiplied by. Hence the round-one + * contributions in the argument list below. + * + * It takes the message and the cosigners' nonce for the same reason, and + * recomputes the challenge from them. It also adds the group's own nonce + * internally, so "the aggregate nonce includes my contribution" holds by + * construction rather than by assumption. + * + * The rule this function cannot enforce for you: never call it twice with the + * same sid32. Not "never with the same message": never twice, whatever else you + * change. A participant's three secrets are fixed by the label alone and + * everything else in the signing equation is a coefficient over public values, + * so two answers under one label are two equations in three unknowns and three + * are three, at which point the key share falls out by elimination. + * + * Half of that is yours: a member can refuse to answer twice if it remembers + * what it has answered under, which means durable storage that is never rolled + * back, because this library holds nothing between calls. Recording the label + * is enough, and it is all that is enough: the label is one signing attempt, + * so a retry is a new label rather than a second answer under the old one. The + * other half is the group's: two members must not answer one label on different + * messages, which no member can detect, since what the others were shown is not + * something the protocol tells it. That is constraint 3 at the top of this file. + * Where the label is a commitment number and an attempt counter, both halves + * come from machinery the surrounding protocol runs anyway, and + * examples/iceberg.c shows the member's half. + * + * What it does check: it builds the group's aggregate from the contributions + * rather than accepting one, because the nesting coefficient is a hash of that + * aggregate and a coordinator free to invent it would hold a coefficient it + * could vary at will. It then interpolates the set and compares the result at + * this participant's own index against the contribution it derives locally, + * which ties the set to the label being signed under. Otherwise a consistent + * sharing from a different session would pass. It also counts the signer among + * the honest points of constraint 2: a set assembled by somebody else has to + * agree with this participant's own share as well as with the rest. The + * participant need not be one of the contributors, since it holds the share + * that determines what its own contribution would have been either way. + * + * Returns: 1 on success, 0 if there are fewer than 2t-1 contributions or more + * than the group size, if two carry the same index, or if the set is + * inconsistent or belongs to another session. The group size is not an + * argument here: it comes off the caller's own share. A malformed + * share, or a share cache built for a different participant, calls the + * illegal callback + * Args: ctx: pointer to a context object (not + * secp256k1_context_static) + * Out: partial_sig: the signature share to publish + * In: share: this participant's share + * cache: its Lagrange weights, or NULL to recompute them + * sid32: the session label, the same one round one used + * pubnonces: the group's own round-one contributions, one per + * member. Not the cosigners', which arrive already + * aggregated as cosigner_aggnonce below. Pass every valid + * one you have; any qualifying set gives the same result, + * so members need not agree on which + * n_pubnonces: how many, at least 2t-1 and at most the group size + * group_pk: the group's public key, the one round one was given + * keyagg_cache: the outer MuSig2 key aggregation cache + * msg32: the message being signed + * cosigner_aggnonce: the cosigners' aggregate nonce, theirs alone. Not the + * other group members', and not including the group's + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8) + SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11); + +/** Check one signature share against what its author published. + * + * Verifies s_k*G == +-(R1,k + b0*b1*R2,k) + e*a*g*gacc*D_k, the equation + * secp256k1_iceberg_partial_sign solves. Without this, + * secp256k1_iceberg_partial_sig_agg turns one bad share into a signature that + * fails to verify with nothing to say why. + * + * The participant's nonce is read off the contributions rather than passed in. + * They determine one polynomial per nonce, and its value at that index is what + * the participant's contribution had to be, so this also works for a member + * that sat out round one and published no contribution of its own. + * + * What a 0 means, and what it does not. It means this share does not satisfy + * that equation against this pubnonce and this public share. It does not name a + * culprit. A MuSig2 partial signature is forgeable by anyone who knows the + * session's public values, so a share that fails here may have been written by + * somebody other than the participant it is attributed to. Assigning blame is a + * stronger claim and needs an honest-supermajority quorum: 3t-2 members + * online at once, rather than the 2t-1 this scheme otherwise requires. Use this + * to find out that a signing attempt will fail before spending an aggregation + * on it, and to narrow where to look. Do not use it as evidence against a + * member. + * + * A 0 also does not distinguish a bad share from bad inputs. The same answer + * comes back if the message or the cosigners' aggregate nonce differs from the + * one the signer had. The set of contributions is the exception: any qualifying + * set from the session determines the same polynomial, so it need not be the + * set the signer used. + * + * Every other argument must be the one secp256k1_iceberg_partial_sign was + * given. The public share is what names the participant, and a signature share + * carrying a different index is refused rather than verified against its own, + * which catches the two arguments being drawn from different members. + * + * Returns: 1 if the share satisfies the equation, 0 otherwise, including a + * contribution count outside 2t-1..n, and including a malformed + * partial_sig, which is the object being examined. A malformed + * pubshare, key aggregation cache, group key or cosigner aggregate is + * the caller's own and calls the illegal callback + * Args: ctx: pointer to a context object + * In: partial_sig: the signature share to check + * pubshare: the public share of the participant it is + * attributed to, which is what says which one that is + * pubnonces: a qualifying set from the same session. It need not + * be the set the signer used: any consistent 2t-1 + * determine the same polynomial + * n_pubnonces: how many, at least 2t-1 and at most n + * n: the group size. A contribution carrying an index + * above it is rejected + * t: the threshold, at least 1 and at most (n+1)/2. An + * n or a t outside its range is a caller bug and + * calls the illegal callback + * group_pk: the group's aggregate public key + * keyagg_cache: the MuSig2 cache for the outer session + * msg32: the message being signed + * cosigner_aggnonce: the cosigners' aggregate nonce + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9) + SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11); + +/** Combine signature shares into one MuSig2 partial signature. + * + * Interpolation needs t points and the quorum bound exists only for the degree + * check, so unlike the nonce round this takes t rather than 2t-1. The scheme + * as specified keeps 2t-1 online throughout regardless; this call simply does + * not need them all to have answered. + * + * The result is an ordinary MuSig2 partial signature. Pass it to + * secp256k1_musig_partial_sig_agg with the cosigners' partial signatures. + * + * Given more than t shares, this refuses a set that contradicts itself. The + * shares of one session lie on a polynomial of degree t-1, as the contributions + * of the nonce round do, so anything past the t needed to interpolate is a + * second opinion and is treated as one. Passing a spare share is therefore + * worth something, and can turn a call that would have succeeded into one that + * fails: at exactly t there is nothing to disagree with, and a bad share is + * aggregated into a signature that fails later without naming a share. + * + * This is not verification. It says the shares agree with each other, not that + * they are the ones the members would have produced, and it cannot tell you + * which of them is the odd one out. secp256k1_iceberg_partial_sig_verify + * answers both, one share at a time, against the public share it names. + * + * Returns: 1 on success, 0 if there are fewer than t shares or more than the + * group size, if a share carries an out-of-range index, if two carry + * the same one, or if more than t shares are given and they do not + * lie on one polynomial of degree t-1. + * Args: ctx: pointer to a context object + * Out: musig_partial_sig: the group's partial signature + * In: partial_sigs: the signature shares. An uninitialized one calls + * the illegal callback; + * a share off the wire is fine, since + * secp256k1_iceberg_partial_sig_parse writes the + * same tag secp256k1_iceberg_partial_sign does + * n_partial_sigs: how many + * n: the group size. A share carrying an index above + * it is rejected + * t: the threshold, at least 1 and at most (n+1)/2. + * An n or a t outside its range is a caller bug + * and calls the illegal callback + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_ICEBERG_H */ diff --git a/include/secp256k1_iceberg_dealer.h b/include/secp256k1_iceberg_dealer.h new file mode 100644 index 00000000..d54b48e9 --- /dev/null +++ b/include/secp256k1_iceberg_dealer.h @@ -0,0 +1,57 @@ +#ifndef SECP256K1_ICEBERG_DEALER_H +#define SECP256K1_ICEBERG_DEALER_H + +#include "secp256k1_iceberg.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** A trusted dealer for Iceberg shares. + * + * This header is deliberately separate from secp256k1_iceberg.h and is not + * installed. Including the module's own header does not offer you a dealer, + * because for the duration of the call below one machine holds everything + * needed to reconstruct the group's private key, which is the situation a + * threshold scheme exists to avoid. + * + * It is here so that the tests, the benchmarks and the example have shares to + * work with. It is also usable where one party is already trusted with the + * whole key, which is a real if narrow case. Anything else wants a distributed + * key generation: the same shares, assembled without the key ever existing in + * one place. This module does not provide one. + * + * Nothing in the signing API depends on how a share was produced. A share + * arrives through secp256k1_iceberg_share_parse, so an externally generated + * one, from a DKG or from another implementation, is used exactly the same + * way. + */ + +/** Deal a group's shares from a single seed. + * + * The seed is the group's private key in all but name until this returns. + * Erase it afterwards, and do not derive it from anything reproducible. + * + * Returns: 1 on success. + * Args: ctx: pointer to a context object + * Out: shares: array of n pointers to share objects, participant k at k-1 + * In: n: number of participants, 1 to + * SECP256K1_ICEBERG_MAX_PARTICIPANTS + * t: threshold, at least 1 and at most (n+1)/2, since a quorum of + * 2t-1 has to fit in the group. So 2-of-2 and 3-of-4 are + * inexpressible + * seed32: 32 bytes of uniformly random data + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_shares_gen( + const secp256k1_context *ctx, + secp256k1_iceberg_share * const *shares, + unsigned int n, + unsigned int t, + const unsigned char *seed32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(5); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_ICEBERG_DEALER_H */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9a66f38f..f84bb377 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,15 @@ if(SECP256K1_ENABLE_MODULE_ELLSWIFT) set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_ellswift.h) endif() +if(SECP256K1_ENABLE_MODULE_ICEBERG) + if(DEFINED SECP256K1_ENABLE_MODULE_MUSIG AND NOT SECP256K1_ENABLE_MODULE_MUSIG) + message(FATAL_ERROR "Module dependency error: You have disabled the musig module explicitly, but it is required by the iceberg module.") + endif() + set(SECP256K1_ENABLE_MODULE_MUSIG ON) + add_compile_definitions(ENABLE_MODULE_ICEBERG=1) + set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_iceberg.h) +endif() + if(SECP256K1_ENABLE_MODULE_MUSIG) if(DEFINED SECP256K1_ENABLE_MODULE_SCHNORRSIG AND NOT SECP256K1_ENABLE_MODULE_SCHNORRSIG) message(FATAL_ERROR "Module dependency error: You have disabled the schnorrsig module explicitly, but it is required by the musig module.") @@ -191,6 +200,10 @@ if(SECP256K1_BUILD_BENCHMARK) target_link_libraries(bench_internal secp256k1_precomputed secp256k1_asm) add_executable(bench_ecmult bench_ecmult.c) target_link_libraries(bench_ecmult secp256k1_precomputed secp256k1_asm) + if(SECP256K1_ENABLE_MODULE_ICEBERG) + add_executable(bench_iceberg bench_iceberg.c) + target_link_libraries(bench_iceberg secp256k1_precomputed secp256k1_asm) + endif() endif() if(SECP256K1_BUILD_TESTS) diff --git a/src/modules/iceberg/Makefile.am.include b/src/modules/iceberg/Makefile.am.include new file mode 100644 index 00000000..a653aee3 --- /dev/null +++ b/src/modules/iceberg/Makefile.am.include @@ -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 diff --git a/src/modules/iceberg/bench_impl.h b/src/modules/iceberg/bench_impl.h new file mode 100644 index 00000000..6b380958 --- /dev/null +++ b/src/modules/iceberg/bench_impl.h @@ -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 + +#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 */ diff --git a/src/modules/iceberg/keygen_impl.h b/src/modules/iceberg/keygen_impl.h new file mode 100644 index 00000000..9467cad6 --- /dev/null +++ b/src/modules/iceberg/keygen_impl.h @@ -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 + +#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 */ diff --git a/src/modules/iceberg/main_impl.h b/src/modules/iceberg/main_impl.h new file mode 100644 index 00000000..34518eb3 --- /dev/null +++ b/src/modules/iceberg/main_impl.h @@ -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 diff --git a/src/modules/iceberg/rss.h b/src/modules/iceberg/rss.h new file mode 100644 index 00000000..04466310 --- /dev/null +++ b/src/modules/iceberg/rss.h @@ -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 + +#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 */ diff --git a/src/modules/iceberg/rss_impl.h b/src/modules/iceberg/rss_impl.h new file mode 100644 index 00000000..9b1d0f5e --- /dev/null +++ b/src/modules/iceberg/rss_impl.h @@ -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 */ diff --git a/src/modules/iceberg/scalar_poly.h b/src/modules/iceberg/scalar_poly.h new file mode 100644 index 00000000..f25d1268 --- /dev/null +++ b/src/modules/iceberg/scalar_poly.h @@ -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 */ diff --git a/src/modules/iceberg/scalar_poly_impl.h b/src/modules/iceberg/scalar_poly_impl.h new file mode 100644 index 00000000..4197dc48 --- /dev/null +++ b/src/modules/iceberg/scalar_poly_impl.h @@ -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 */ diff --git a/src/modules/iceberg/session_impl.h b/src/modules/iceberg/session_impl.h new file mode 100644 index 00000000..781d1f73 --- /dev/null +++ b/src/modules/iceberg/session_impl.h @@ -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 + +#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 */ diff --git a/src/modules/iceberg/tests_impl.h b/src/modules/iceberg/tests_impl.h new file mode 100644 index 00000000..30d3ad95 --- /dev/null +++ b/src/modules/iceberg/tests_impl.h @@ -0,0 +1,2432 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_TESTS_IMPL_H +#define SECP256K1_MODULE_ICEBERG_TESTS_IMPL_H + +#include + +#include "../../../include/secp256k1_iceberg.h" +#include "../../../include/secp256k1_iceberg_dealer.h" + +#include "../../scalar.h" +#include "../../testrand.h" +#include "../../testutil.h" +#include "../../unit_test.h" + +#include "vectors.h" + +/* The configuration tables below are written for a participant bound of ten. + * That bound is fixed in the public header, so every one of these is false + * today; they are here so that a build which edits the #define runs the subset + * of each table that still fits instead of tripping an ARG_CHECK. */ +static int iceberg_test_fits(unsigned int n) { + return n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; +} + +/* Horner evaluation, used only to check the library's own results against the + * defining property of whatever it just computed. */ +static void iceberg_test_poly_eval(secp256k1_scalar *r, const secp256k1_scalar *coeffs, size_t n_coeffs, unsigned int at) { + secp256k1_scalar x; + size_t i; + + secp256k1_scalar_set_int(&x, at); + secp256k1_scalar_set_int(r, 0); + for (i = n_coeffs; i > 0; i--) { + secp256k1_scalar_mul(r, r, &x); + secp256k1_scalar_add(r, r, &coeffs[i - 1]); + } +} + +static void iceberg_test_random_scalar(secp256k1_scalar *r) { + unsigned char buf[32]; + testrand256(buf); + secp256k1_scalar_set_b32(r, buf, NULL); +} + +/* The outer MuSig2 half of a session, which most of the tests below need before + * they can reach an iceberg call at all: a cosigner, and the aggregate of its + * key with the group's. The group's key enters as an ordinary public key. */ +static void iceberg_test_cosigner_key(secp256k1_keypair *keypair, secp256k1_pubkey *pk, + secp256k1_xonly_pubkey *agg_xonly, + secp256k1_musig_keyagg_cache *cache, + const secp256k1_pubkey *group_pk, + const unsigned char *seckey) { + const secp256k1_pubkey *pubkeys[2]; + + CHECK(secp256k1_keypair_create(CTX, keypair, seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, pk, keypair) == 1); + pubkeys[0] = group_pk; + pubkeys[1] = pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, agg_xonly, cache, pubkeys, 2) == 1); +} + +/* The cosigners' aggregate nonce, where the cosigners are the one made above. + * An aggregate of one is not a special case anywhere. */ +static void iceberg_test_cosigner_nonce(secp256k1_musig_secnonce *secnonce, + secp256k1_musig_pubnonce *pubnonce, + secp256k1_musig_aggnonce *aggnonce, + unsigned char *secrand, const unsigned char *seckey, + const secp256k1_pubkey *pk, const unsigned char *msg, + const secp256k1_musig_keyagg_cache *cache) { + const secp256k1_musig_pubnonce *just_cosigner[1]; + + CHECK(secp256k1_musig_nonce_gen(CTX, secnonce, pubnonce, secrand, seckey, pk, msg, cache, NULL) == 1); + just_cosigner[0] = pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, aggnonce, just_cosigner, 1) == 1); +} + +/* A polynomial built from roots must vanish at each of them, and not at a + * non-root: each round probes x = 11, which is never one. */ +static void run_iceberg_from_roots_test(void) { + unsigned char roots[5] = { 1, 3, 4, 7, 9 }; + secp256k1_scalar coeffs[6], value; + size_t i, j; + + for (i = 1; i <= 5; i++) { + secp256k1_scalarpoly_from_roots_var(coeffs, roots, i); + for (j = 0; j < i; j++) { + iceberg_test_poly_eval(&value, coeffs, i + 1, roots[j]); + CHECK(secp256k1_scalar_is_zero(&value)); + } + /* Monic, so the leading coefficient is one and the degree is exact. */ + CHECK(secp256k1_scalar_is_one(&coeffs[i])); + /* An index that is not a root must not vanish. */ + iceberg_test_poly_eval(&value, coeffs, i + 1, 11); + CHECK(!secp256k1_scalar_is_zero(&value)); + } +} + +/* Dividing out a root and multiplying it back must return the original. */ +static void run_iceberg_div_root_test(void) { + unsigned char roots[4] = { 2, 5, 6, 8 }; + secp256k1_scalar master[5], quotient[4], value, expected; + size_t j, at; + + secp256k1_scalarpoly_from_roots_var(master, roots, 4); + for (j = 0; j < 4; j++) { + secp256k1_scalarpoly_div_root_var(quotient, master, 4, roots[j]); + /* q(x)*(x - root) == master(x), checked at a handful of points. */ + for (at = 1; at <= 12; at++) { + secp256k1_scalar factor; + iceberg_test_poly_eval(&value, quotient, 4, (unsigned int)at); + secp256k1_scalarpoly_small_diff(&factor, (unsigned int)at, roots[j]); + secp256k1_scalar_mul(&value, &value, &factor); + iceberg_test_poly_eval(&expected, master, 5, (unsigned int)at); + CHECK(secp256k1_scalar_eq(&value, &expected)); + } + } +} + +/* The two uses of lagrange_eval_var, each against an independent computation of + * the same weight. These are the identities the whole scheme leans on, and both + * routes below share no code with lagrange_parts_var. + * + * L_a(a) == 1 is not tested here: at == exclude makes the numerator and + * denominator the same product, so it asserts x/x == 1 and any mutation that + * changes both loops together still satisfies it. */ +static void run_iceberg_lagrange_eval_test(void) { + unsigned char set[3] = { 2, 4, 5 }; + unsigned char quorum[4] = { 1, 3, 4, 6 }; + secp256k1_scalar poly[4], basis[4 * 4], weight, expect, scale, total; + unsigned int at; + size_t j; + + /* exclude = 0 is the replicated-sharing weight L_a, which rss_impl.h evaluates + * at a participant index outside a. Written as a polynomial the product over a + * is monic, so L_a(x) = P_a(x)/P_a(0) with P_a from from_roots_var, which + * run_iceberg_from_roots_test pins separately. L_a vanishing on every + * member of a (why a party can evaluate its own share without the seeds it + * lacks) is the special case P_a(i) == 0, so it is covered here too. */ + secp256k1_scalarpoly_from_roots_var(poly, set, 3); + iceberg_test_poly_eval(&scale, poly, 4, 0); + secp256k1_scalar_inverse_var(&scale, &scale); + for (at = 0; at <= 9; at++) { + secp256k1_scalarpoly_lagrange_eval_var(&weight, set, 3, 0, at); + iceberg_test_poly_eval(&expect, poly, 4, at); + secp256k1_scalar_mul(&expect, &expect, &scale); + CHECK(secp256k1_scalar_eq(&weight, &expect)); + } + + /* exclude = j is the reconstruction weight lambda_j. Nothing above reaches the + * skip in lagrange_parts_var, because 0 is never one of the indices; here it is + * taken on every iteration. lagrange_basis_var builds the same polynomials + * coefficient by coefficient and its own test pins it against poly_eval. */ + secp256k1_scalarpoly_lagrange_basis_var(basis, quorum, 4); + for (j = 0; j < 4; j++) { + for (at = 0; at <= 9; at++) { + secp256k1_scalarpoly_lagrange_eval_var(&weight, quorum, 4, quorum[j], at); + iceberg_test_poly_eval(&expect, &basis[j * 4], 4, at); + CHECK(secp256k1_scalar_eq(&weight, &expect)); + } + } + + /* And the reconstruction weights sum to one, for any quorum. */ + secp256k1_scalar_set_int(&total, 0); + for (j = 0; j < 4; j++) { + secp256k1_scalarpoly_lagrange_eval_var(&weight, quorum, 4, quorum[j], 0); + secp256k1_scalar_add(&total, &total, &weight); + } + CHECK(secp256k1_scalar_is_one(&total)); +} + +/* Each basis polynomial is one at its own node and zero at the others. */ +static void run_iceberg_lagrange_basis_test(void) { + unsigned char quorum[5] = { 1, 2, 4, 7, 9 }; + secp256k1_scalar basis[5 * 5], value; + size_t m, j, i; + + for (m = 1; m <= 5; m++) { + secp256k1_scalarpoly_lagrange_basis_var(basis, quorum, m); + for (j = 0; j < m; j++) { + for (i = 0; i < m; i++) { + iceberg_test_poly_eval(&value, &basis[j * m], m, quorum[i]); + if (i == j) { + CHECK(secp256k1_scalar_is_one(&value)); + } else { + CHECK(secp256k1_scalar_is_zero(&value)); + } + } + } + } +} + +/* Interpolating m evaluations of a degree m-1 polynomial must return its + * constant term. + * + * This runs all the way to SECP256K1_SCALARPOLY_MAX_POINTS deliberately. The + * Python reference computes the same weights through numpy float64 and starts + * returning wrong answers from a quorum of 19, with no indication that anything + * went wrong. Working in the scalar field makes the size irrelevant, and testing + * at the maximum is how we keep it that way. */ +static void run_iceberg_interpolate_test_internal(void) { + unsigned char quorum[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar coeffs[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar vals[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar recovered; + size_t m, i, trial; + + /* Spread the indices rather than using 1..m, so a bug that only shows up + * with consecutive nodes has somewhere to surface. */ + for (i = 0; i < SECP256K1_SCALARPOLY_MAX_POINTS; i++) { + quorum[i] = (unsigned char)(2 * i + 1); + } + + for (m = 1; m <= SECP256K1_SCALARPOLY_MAX_POINTS; m++) { + for (trial = 0; trial < 2; trial++) { + for (i = 0; i < m; i++) { + iceberg_test_random_scalar(&coeffs[i]); + } + for (i = 0; i < m; i++) { + iceberg_test_poly_eval(&vals[i], coeffs, m, quorum[i]); + } + secp256k1_scalarpoly_interpolate_at0(&recovered, quorum, vals, m); + CHECK(secp256k1_scalar_eq(&recovered, &coeffs[0])); + } + } +} + +static void run_iceberg_batch_inverse_test_internal(void) { + secp256k1_scalar values[6], batched[6], single; + size_t len, i; + + for (len = 1; len <= 6; len++) { + for (i = 0; i < len; i++) { + do { + iceberg_test_random_scalar(&values[i]); + } while (secp256k1_scalar_is_zero(&values[i])); + } + secp256k1_scalarpoly_inverse_batch_var(batched, values, len); + for (i = 0; i < len; i++) { + secp256k1_scalar_inverse_var(&single, &values[i]); + CHECK(secp256k1_scalar_eq(&batched[i], &single)); + } + /* In-place must give the same answer, since callers will want it. */ + secp256k1_scalarpoly_inverse_batch_var(values, values, len); + for (i = 0; i < len; i++) { + CHECK(secp256k1_scalar_eq(&batched[i], &values[i])); + } + } +} + +/* The binomial is computed rather than tabulated, so check it against the + * recurrence that defines it, and confirm the derived bounds in rss.h really + * are the worst case they claim to be. */ +static void run_iceberg_binom_test(void) { + unsigned int n, k, max_t = 0; + uint32_t max_summands = 0, max_subsets = 0; + + for (n = 0; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + CHECK(secp256k1_rss_binom(n, 0) == 1); + CHECK(secp256k1_rss_binom(n, n) == 1); + for (k = 1; k < n; k++) { + CHECK(secp256k1_rss_binom(n, k) + == secp256k1_rss_binom(n - 1, k - 1) + secp256k1_rss_binom(n - 1, k)); + } + } + + /* A quorum of 2t-1 must fit in the group, so t <= (n+1)/2. Over every + * expressible configuration, no threshold exceeds MAX_T, no participant + * holds more seeds than MAX_SUMMANDS and no group has more subsets than + * MAX_SUBSETS. All three are written out by hand, since C89 cannot evaluate + * a binomial at preprocessing time, so all three are recomputed here. + * + * Each is pinned by equality rather than bounded. Too small overruns an + * array and the bound alone would catch that; too large only wastes space, + * which is a real thing to get told about when the participant bound has + * been lowered and one of these was left behind. */ + for (n = 1; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + unsigned int t; + for (t = 1; 2 * t - 1 <= n; t++) { + CHECK(t <= SECP256K1_ICEBERG_MAX_T); + if (t > max_t) { + max_t = t; + } + if (secp256k1_rss_binom(n - 1, t - 1) > max_summands) { + max_summands = secp256k1_rss_binom(n - 1, t - 1); + } + if (secp256k1_rss_binom(n, t - 1) > max_subsets) { + max_subsets = secp256k1_rss_binom(n, t - 1); + } + } + } + CHECK(max_t == SECP256K1_ICEBERG_MAX_T); + CHECK(max_summands == SECP256K1_ICEBERG_MAX_SUMMANDS); + CHECK(max_subsets == SECP256K1_ICEBERG_MAX_SUBSETS); +} + +/* Ranking fixes the wire layout of a share, so it has to be a bijection and it + * has to agree with lexicographic order. Both checked exhaustively. */ +static void run_iceberg_subset_rank_test(void) { + unsigned int n, size; + + for (n = 1; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + for (size = 1; size <= n && size <= SECP256K1_ICEBERG_MAX_T; size++) { + uint32_t total = secp256k1_rss_binom(n, size); + unsigned char previous[SECP256K1_ICEBERG_MAX_T]; + unsigned char current[SECP256K1_ICEBERG_MAX_T]; + uint32_t rank; + + for (rank = 0; rank < total; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(n, size, rank); + size_t members = secp256k1_rss_subset_members(current, subset, n); + CHECK(members == size); + CHECK(secp256k1_rss_subset_rank(n, size, subset) == rank); + if (rank > 0) { + /* Strictly increasing in lexicographic order. */ + size_t i = 0; + while (i < size && previous[i] == current[i]) { + i++; + } + CHECK(i < size && previous[i] < current[i]); + } + memcpy(previous, current, size); + } + } + } +} + +/* Known answers taken from the reference implementation's own prf(). If these + * drift, shares stop being portable between implementations. */ +static void run_iceberg_prf_test(void) { + static const unsigned char expected_hello[32] = { + 0xdf,0x15,0xf1,0x11,0xc7,0xd3,0xe8,0x7e,0x32,0x58,0x82,0x72,0xa0,0x9d,0x4f,0x25, + 0xaa,0x27,0x35,0xb5,0x4d,0x56,0x7e,0xbc,0xcd,0x10,0x29,0x79,0x01,0x74,0x71,0xbe + }; + static const unsigned char expected_empty[32] = { + 0x20,0x15,0x27,0x95,0xd7,0x00,0x9c,0x6f,0x3c,0x7c,0x20,0x69,0x3e,0xf9,0xfc,0x51, + 0x63,0x8c,0x64,0x31,0x8e,0xb1,0xc2,0x65,0x1f,0x5f,0xd2,0x48,0xaf,0x05,0x9f,0x14 + }; + unsigned char seed[32], out[32]; + secp256k1_scalar r; + + memset(seed, 0x01, sizeof(seed)); + secp256k1_rss_prf(secp256k1_get_hash_context(CTX), &r, seed, (const unsigned char *)"hello", 5); + secp256k1_scalar_get_b32(out, &r); + CHECK(secp256k1_memcmp_var(out, expected_hello, 32) == 0); + + memset(seed, 0xab, sizeof(seed)); + secp256k1_rss_prf(secp256k1_get_hash_context(CTX), &r, seed, NULL, 0); + secp256k1_scalar_get_b32(out, &r); + CHECK(secp256k1_memcmp_var(out, expected_empty, 32) == 0); + +} + +/* Advance `chosen` to the next size-`size` subset of {1..n}, ascending. + * Returns 0 once the last one has been passed. */ +static int iceberg_test_next_combination(unsigned char *chosen, size_t size, unsigned int n) { + size_t i = size; + + while (i > 0) { + i--; + if (chosen[i] < n - (size - 1 - i)) { + size_t j; + chosen[i]++; + for (j = i + 1; j < size; j++) { + chosen[j] = chosen[j - 1] + 1; + } + return 1; + } + } + return 0; +} + +/* Every precomputed midstate equals its tag applied at runtime. + * + * Each of the four is eight magic constants a person pasted in, and three of + * them are only pinned by accident: dealer by the known-answer test, noncecoef + * by the vectors, prf by the two digests above. Iceberg/batchcoef was pinned by + * nothing at all, because the batching coefficient only has to be unpredictable: + * any midstate produces a sound check, so a wrong one would have been invisible + * here and two implementations would have disagreed in silence. */ +static void run_iceberg_midstate_test(void) { + static void (* const tagged[])(secp256k1_sha256 *) = { + secp256k1_rss_prf_sha256_tagged, + secp256k1_vpss_batchcoef_sha256_tagged, + secp256k1_iceberg_dealer_sha256_tagged, + secp256k1_iceberg_noncecoef_sha256_tagged + }; + static const char * const tags[] = { + "VPSS/prf", "Iceberg/batchcoef", "Iceberg/dealer", "Iceberg/noncecoef" + }; + unsigned char probe[32]; + size_t i; + + memset(probe, 0x5e, sizeof(probe)); + for (i = 0; i < sizeof(tags) / sizeof(tags[0]); i++) { + secp256k1_sha256 from_midstate, from_tag; + unsigned char a[32], b[32]; + + tagged[i](&from_midstate); + secp256k1_sha256_initialize_tagged(secp256k1_get_hash_context(CTX), &from_tag, + (const unsigned char *)tags[i], strlen(tags[i])); + secp256k1_sha256_write(secp256k1_get_hash_context(CTX), &from_midstate, probe, sizeof(probe)); + secp256k1_sha256_write(secp256k1_get_hash_context(CTX), &from_tag, probe, sizeof(probe)); + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &from_midstate, a); + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &from_tag, b); + CHECK(secp256k1_memcmp_var(a, b, 32) == 0); + } +} + +/* The outer key aggregation, which partial_sign cannot check for itself. + * + * It takes the group's public key and the MuSig2 cache as two separate arguments + * and nothing ties them together, because nothing can: a cache records the hash + * of the key list rather than the list. keyagg_check answers the question + * directly, and belongs where the cache is built rather than once per signature, + * since the key set belongs to the channel. */ +static void run_iceberg_keyagg_check_test(void) { + enum { N = 7, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[MU]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_keypair cosigner_keypair; + secp256k1_pubkey group_pk, cosigner_pk, impostor; + const secp256k1_pubkey *pubkeys[2], *wrong[2]; + unsigned char seed[32], seckey[32], tweak[32]; + unsigned int k; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x11, sizeof(seed)); + memset(seckey, 0x22, sizeof(seckey)); + + for (k = 1; k <= N; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + } + for (i = 0; i < MU; i++) { + pubshare_ptrs[i] = &pubshares[i]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + CHECK(secp256k1_keypair_create(CTX, &cosigner_keypair, seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, &cosigner_pk, &cosigner_keypair) == 1); + /* A key the signer has no way to check against anything it holds. */ + seckey[0] ^= 1; + CHECK(secp256k1_keypair_create(CTX, &cosigner_keypair, seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, &impostor, &cosigner_keypair) == 1); + + pubkeys[0] = &group_pk; + pubkeys[1] = &cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, NULL, &keyagg_cache, pubkeys, 2) == 1); + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk) == 1); + + /* The same two keys in the other order hash to something else. */ + pubkeys[0] = &cosigner_pk; + pubkeys[1] = &group_pk; + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk) == 0); + + /* The right list, but the group is not in it. */ + pubkeys[0] = &group_pk; + pubkeys[1] = &cosigner_pk; + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &impostor) == 0); + + /* A cosigner the cache was not built over. */ + wrong[0] = &group_pk; + wrong[1] = &impostor; + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, wrong, 2, &group_pk) == 0); + + /* Tweaks do not disturb it: the list hash is fixed at aggregation. */ + memset(tweak, 0x6e, sizeof(tweak)); + CHECK(secp256k1_musig_pubkey_xonly_tweak_add(CTX, NULL, &keyagg_cache, tweak) == 1); + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk) == 1); +} + + +/* The identity the whole scheme rests on: participants who never communicate + * produce evaluations of one degree t-1 polynomial whose constant term is the + * sum of the hashes of every seed. Checked for every quorum, so "any t of them + * agree" is exhaustive rather than sampled. */ +static void run_iceberg_pss_test_internal(void) { + static const unsigned char configs[][2] = { {3,2}, {4,2}, {5,3}, {6,3}, {7,4} }; + unsigned char seeds[SECP256K1_ICEBERG_MAX_SUBSETS][32]; + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SUMMANDS]; + unsigned char held[SECP256K1_ICEBERG_MAX_SUMMANDS * 32]; + secp256k1_scalar shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1]; + unsigned char label[8] = "label"; + size_t config; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1]; + uint32_t total = secp256k1_rss_binom(n, t - 1); + secp256k1_scalar expected, term, recovered; + unsigned char quorum[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar quorum_vals[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + uint32_t rank; + unsigned int k; + size_t i; + + if (!iceberg_test_fits(n)) { + continue; + } + + for (rank = 0; rank < total; rank++) { + testrand256(seeds[rank]); + } + + /* f_w(0) is just the sum over every seed. No participant computes this, + * and no participant could. */ + secp256k1_scalar_set_int(&expected, 0); + for (rank = 0; rank < total; rank++) { + secp256k1_rss_prf(secp256k1_get_hash_context(CTX), &term, seeds[rank], label, 5); + secp256k1_scalar_add(&expected, &expected, &term); + } + + for (k = 1; k <= n; k++) { + size_t count = secp256k1_rss_lagrange_weights_var(weights, n, t, k); + size_t got = 0; + CHECK(count == secp256k1_rss_binom(n - 1, t - 1)); + /* Gather this participant's seeds in the same rank order the weights + * were produced in. */ + for (rank = 0; rank < total; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(n, t - 1, rank); + if (subset & (secp256k1_rss_subset)(1u << k)) { + continue; + } + memcpy(&held[32 * got], seeds[rank], 32); + got++; + } + CHECK(got == count); + secp256k1_rss_eval(secp256k1_get_hash_context(CTX), &shares[k], held, weights, count, label, 5); + } + + /* Every t-subset of participants must reconstruct the same value. */ + for (i = 0; i < t; i++) { + quorum[i] = (unsigned char)(i + 1); + } + do { + for (i = 0; i < t; i++) { + quorum_vals[i] = shares[quorum[i]]; + } + secp256k1_scalarpoly_interpolate_at0(&recovered, quorum, quorum_vals, t); + CHECK(secp256k1_scalar_eq(&recovered, &expected)); + } while (iceberg_test_next_combination(quorum, t, n)); + } +} + +/* The degree check, written out the way the definition reads: one + * multiexponentiation per high coefficient, no batching, no transcript. + * + * This exists only to disagree with the batched implementation if the batching + * is ever wrong, so it is deliberately a transcription of the defining property + * rather than a refactor of the real code. It does share lagrange_basis_var with + * the real one, which run_iceberg_lagrange_basis_test pins separately against + * its defining property; an error there would pass both of these. + */ +static int iceberg_test_verify_naive(const unsigned char *idx, const secp256k1_ge *points, size_t m, unsigned int t) { + secp256k1_scalar basis[SECP256K1_ICEBERG_MAX_PARTICIPANTS * SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + size_t i, j; + + secp256k1_scalarpoly_lagrange_basis_var(basis, idx, m); + for (i = t; i < m; i++) { + secp256k1_gej acc; + secp256k1_gej_set_infinity(&acc); + for (j = 0; j < m; j++) { + secp256k1_gej term, pointj; + secp256k1_gej_set_ge(&pointj, &points[j]); + secp256k1_ecmult(&term, &pointj, &basis[j * m + i], NULL); + secp256k1_gej_add_var(&acc, &acc, &term, NULL); + } + if (!secp256k1_gej_is_infinity(&acc)) { + return 0; + } + } + return 1; +} + +/* Commit to a polynomial: points[i] <- f(idx[i]) * G. */ +static void iceberg_test_commitments(secp256k1_ge *points, const unsigned char *idx, size_t m, const secp256k1_scalar *coeffs, size_t degree_plus_one) { + size_t i; + for (i = 0; i < m; i++) { + secp256k1_scalar value; + secp256k1_gej pointj; + iceberg_test_poly_eval(&value, coeffs, degree_plus_one, idx[i]); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &pointj, &value); + secp256k1_ge_set_gej(&points[i], &pointj); + } +} + +static void run_iceberg_vpss_test_internal(void) { + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge points[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar coeffs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned int t; + size_t m, i; + + for (i = 0; i < SECP256K1_ICEBERG_MAX_PARTICIPANTS; i++) { + idx[i] = (unsigned char)(i + 1); + } + + for (t = 2; t <= 4; t++) { + for (m = 2 * t - 1; m <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; m++) { + secp256k1_ge tampered[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_gej combined, expected_gej; + secp256k1_ge expected; + secp256k1_scalar high[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + + for (i = 0; i < t; i++) { + iceberg_test_random_scalar(&coeffs[i]); + } + iceberg_test_commitments(points, idx, m, coeffs, t); + + /* Honest commitments verify, and the batched check agrees with the + * term-by-term one. */ + CHECK(secp256k1_vpss_verify_var(CTX, idx, points, m, t) == 1); + CHECK(iceberg_test_verify_naive(idx, points, m, t) == 1); + + /* Combining gives the commitment to the secret, from any quorum. */ + CHECK(secp256k1_vpss_combine_var(CTX, &combined, idx, points, m) == 1); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &expected_gej, &coeffs[0]); + secp256k1_ge_set_gej(&expected, &expected_gej); + CHECK(secp256k1_gej_eq_ge_var(&combined, &expected)); + + /* One participant lying pushes the degree up and is caught. Both + * implementations must agree that it was. */ + memcpy(tampered, points, sizeof(secp256k1_ge) * m); + { + secp256k1_gej bumped; + secp256k1_scalar delta; + iceberg_test_random_scalar(&delta); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &bumped, &delta); + secp256k1_gej_add_ge_var(&bumped, &bumped, &tampered[m / 2], NULL); + secp256k1_ge_set_gej(&tampered[m / 2], &bumped); + } + CHECK(secp256k1_vpss_verify_var(CTX, idx, tampered, m, t) == 0); + CHECK(iceberg_test_verify_naive(idx, tampered, m, t) == 0); + + /* A genuine degree-t sharing must also fail. The tamper case + * above only shows that inconsistent points are caught; this shows + * the check bounds the degree rather than merely testing that the + * points lie on some polynomial. */ + if (m > t) { + for (i = 0; i <= t; i++) { + iceberg_test_random_scalar(&high[i]); + } + do { + iceberg_test_random_scalar(&high[t]); + } while (secp256k1_scalar_is_zero(&high[t])); + iceberg_test_commitments(points, idx, m, high, t + 1); + CHECK(secp256k1_vpss_verify_var(CTX, idx, points, m, t) == 0); + CHECK(iceberg_test_verify_naive(idx, points, m, t) == 0); + } + } + } +} + +/* Any quorum, in any order, must combine to the same point. This is the + * property that lets different participants show up in each signing round. */ +static void run_iceberg_vpss_quorum_test_internal(void) { + unsigned char all[7], quorum[7]; + secp256k1_ge points[7], chosen[7]; + secp256k1_scalar coeffs[3]; + secp256k1_gej expected, combined; + unsigned int t = 3, n = 7; + size_t i; + + if (!iceberg_test_fits(n)) { + return; + } + + for (i = 0; i < n; i++) { + all[i] = (unsigned char)(i + 1); + } + for (i = 0; i < t; i++) { + iceberg_test_random_scalar(&coeffs[i]); + } + iceberg_test_commitments(points, all, n, coeffs, t); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &expected, &coeffs[0]); + + for (i = 0; i < t; i++) { + quorum[i] = (unsigned char)(i + 1); + } + do { + for (i = 0; i < t; i++) { + chosen[i] = points[quorum[i] - 1]; + } + CHECK(secp256k1_vpss_combine_var(CTX, &combined, quorum, chosen, t) == 1); + { + secp256k1_ge got; + secp256k1_ge_set_gej(&got, &combined); + CHECK(secp256k1_gej_eq_ge_var(&expected, &got)); + } + /* The same quorum arranged backwards. next_combination only ever hands + * out ascending sets, so without this the "in any order" above would be + * an assumption: the weights are a function of the index set, not of how + * it is laid out in the array. */ + { + unsigned char reversed[7]; + secp256k1_ge backwards[7]; + secp256k1_ge got; + for (i = 0; i < t; i++) { + reversed[i] = quorum[t - 1 - i]; + backwards[i] = points[reversed[i] - 1]; + } + CHECK(secp256k1_vpss_combine_var(CTX, &combined, reversed, backwards, t) == 1); + secp256k1_ge_set_gej(&got, &combined); + CHECK(secp256k1_gej_eq_ge_var(&expected, &got)); + } + } while (iceberg_test_next_combination(quorum, t, n)); +} + +/* Deal a group and check the aggregated public key really is the shared secret. + * + * The secret is reconstructed here only to confirm the public path agrees with + * it. No participant ever computes this value, and the API offers no way to. */ +static void run_iceberg_keygen_test_internal(void) { + static const unsigned char configs[][2] = { {3,2}, {5,3}, {5,2}, {7,4}, {9,5}, {10,5} }; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share_cache caches[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned char seed[32]; + size_t config, i; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1]; + unsigned int mu = 2 * t - 1; + secp256k1_pubkey group_pk, again; + secp256k1_scalar secret_shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1]; + secp256k1_scalar recovered; + unsigned char quorum[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar quorum_vals[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_gej expected_gej; + secp256k1_ge expected, got; + unsigned int k; + + if (!iceberg_test_fits(n)) { + continue; + } + + testrand256(seed); + for (k = 1; k <= n; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, n, t, seed) == 1); + + for (k = 1; k <= n; k++) { + const unsigned char *seeds; + unsigned int gn, gt, gk; + size_t count; + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS]; + + CHECK(secp256k1_iceberg_share_cache_create(CTX, &caches[k - 1], &shares[k - 1]) == 1); + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], &caches[k - 1]) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + + /* Reconstruct this participant's secret share, for the check below. */ + count = secp256k1_iceberg_share_load(CTX, &gn, >, &gk, &seeds, &shares[k - 1]); + CHECK(count > 0 && gn == n && gt == t && gk == k); + CHECK(secp256k1_rss_lagrange_weights_var(weights, n, t, k) == count); + secp256k1_rss_eval(secp256k1_get_hash_context(CTX), &secret_shares[k], seeds, weights, + count, secp256k1_iceberg_keygen_label, + sizeof(secp256k1_iceberg_keygen_label)); + } + + /* A quorum of exactly 2t-1 aggregates to the group key. */ + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, mu, n, t) == 1); + + /* And that key is the commitment to the reconstructed secret. */ + for (i = 0; i < t; i++) { + quorum[i] = (unsigned char)(i + 1); + quorum_vals[i] = secret_shares[i + 1]; + } + secp256k1_scalarpoly_interpolate_at0(&recovered, quorum, quorum_vals, t); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &expected_gej, &recovered); + secp256k1_ge_set_gej(&expected, &expected_gej); + CHECK(secp256k1_pubkey_load(CTX, &got, &group_pk)); + CHECK(secp256k1_ge_eq_var(&expected, &got)); + + /* The full group gives the same answer as the 2t-1 quorum. */ + if (n > mu) { + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &again, pubshare_ptrs, n, n, t) == 1); + CHECK(secp256k1_memcmp_var(&group_pk, &again, sizeof(group_pk)) == 0); + } + + /* Fewer than 2t-1 shares proves nothing and must be refused. */ + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &again, pubshare_ptrs, mu - 1, n, t) == 0); + + /* Recomputing the weights must match using the cache. Against a saved + * copy, not against pubshare_ptrs[0]: that points at pubshares[0], which + * this line overwrites. */ + { + secp256k1_iceberg_pubshare with_cache = pubshares[0]; + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[0], &shares[0], NULL) == 1); + CHECK(secp256k1_memcmp_var(&pubshares[0], &with_cache, sizeof(pubshares[0])) == 0); + } + } +} + +/* Serialization round-trips, and the configurations the scheme cannot express + * are rejected at the point of dealing rather than later. */ +static void run_iceberg_keygen_api_test(void) { + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share parsed; + secp256k1_iceberg_share_cache cache; + secp256k1_iceberg_pubshare pubshare, reparsed; + unsigned char seed[32], buf[4 + 32 * SECP256K1_ICEBERG_MAX_SEEDS], wire[34]; + size_t len, i; + + /* The only group this deals is five; the larger ones below are refusals. */ + if (!iceberg_test_fits(5)) { + return; + } + + memset(seed, 0x7c, sizeof(seed)); + for (i = 0; i < SECP256K1_ICEBERG_MAX_PARTICIPANTS; i++) { + ptrs[i] = &shares[i]; + } + + /* Minority threshold only: t <= (n+1)/2, because a quorum of 2t-1 has to + * fit in the group. These are the configurations people ask for first. */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, 2, 2, seed)); /* 2-of-2 */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, 4, 3, seed)); /* 3-of-4 */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, 6, 4, seed)); /* 4-of-6 */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1, 2, seed)); + + CHECK(secp256k1_iceberg_shares_gen(CTX, ptrs, 5, 3, seed) == 1); + + /* Share round-trip, and a serialized share is exactly as large as it needs + * to be rather than as large as the opaque type. */ + len = sizeof(buf); + CHECK(secp256k1_iceberg_share_serialize(CTX, buf, &len, &shares[2]) == 1); + CHECK(len == 4 + 32 * secp256k1_rss_binom(4, 2)); + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len) == 1); + CHECK(secp256k1_memcmp_var(&parsed, &shares[2], sizeof(parsed)) == 0); + + /* Truncated, over-long and wrong-version inputs are all refused. The + * over-long case matters on its own: a caller pulling a share out of a + * longer record and passing the whole buffer must not have the tail + * ignored. */ + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len - 1) == 0); + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len + 1) == 0); + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, 3) == 0); + buf[0] = 2; + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len) == 0); + buf[0] = 1; + + /* Too small a buffer reports the size it wanted. */ + len = 4; + CHECK(secp256k1_iceberg_share_serialize(CTX, buf, &len, &shares[2]) == 0); + CHECK(len == 4 + 32 * secp256k1_rss_binom(4, 2)); + + /* A malformed share has no length to report, so it reports zero. A caller + * that sizes a second call from *outlen, which is what the short-buffer case + * above teaches it to do, would otherwise loop on the stale value + * forever. */ + { + secp256k1_iceberg_share broken = shares[2]; + broken.data[0] ^= 1; + len = sizeof(buf); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_share_serialize(CTX, buf, &len, &broken)); + CHECK(len == 0); + } + + /* SECP256K1_ICEBERG_SHARE_MAX_LEN is what the header promises callers can + * size a buffer with, so it has to bound every configuration and not just + * the ones above. The per-configuration check stays even though the last + * line implies it: it fires on the offending n and t, where the other only + * says the maximum came out wrong. */ + { + unsigned int n, t; + size_t largest = 0; + for (n = 2; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + for (t = 1; 2 * t - 1 <= n; t++) { + size_t need = 4 + 32 * secp256k1_rss_binom(n - 1, t - 1); + CHECK(need <= SECP256K1_ICEBERG_SHARE_MAX_LEN); + if (need > largest) { + largest = need; + } + } + } + CHECK(largest == SECP256K1_ICEBERG_SHARE_MAX_LEN); + } + + /* A cache belonging to another participant must not be usable, since it + * would silently produce a wrong share rather than an error. */ + CHECK(secp256k1_iceberg_share_cache_create(CTX, &cache, &shares[0]) == 1); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_pubshare_gen(CTX, &pubshare, &shares[1], &cache)); + + /* Public share round-trip. */ + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshare, &shares[0], NULL) == 1); + CHECK(secp256k1_iceberg_pubshare_serialize(CTX, wire, &pubshare) == 1); + CHECK(secp256k1_iceberg_pubshare_parse(CTX, &reparsed, wire) == 1); + CHECK(secp256k1_memcmp_var(&reparsed, &pubshare, sizeof(pubshare)) == 0); + wire[0] = 0; + CHECK(secp256k1_iceberg_pubshare_parse(CTX, &reparsed, wire) == 0); +} + +/* The three objects that cross a network survive the trip, and the encodings + * that would be ambiguous or unusable on the far side are refused. */ +static void run_iceberg_wire_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubnonce nonces[N], parsed_nonce; + const secp256k1_iceberg_pubnonce *nonce_ptrs[N]; + secp256k1_iceberg_aggnonce aggnonce, parsed_agg; + secp256k1_iceberg_partial_sig psig, parsed_psig; + secp256k1_musig_pubnonce group_pubnonce; + secp256k1_pubkey group_pk; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N]; + unsigned char seed[32], sid[32]; + unsigned char nonce67[67], agg66[66], psig33[33]; + unsigned int k; + + if (!iceberg_test_fits(N)) { + return; + } + + memset(seed, 0x5e, sizeof(seed)); + memset(sid, 0x9a, sizeof(sid)); + for (k = 0; k < N; k++) { + share_ptrs[k] = &shares[k]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (k = 0; k < N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k], &shares[k], NULL) == 1); + pubshare_ptrs[k] = &pubshares[k]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + /* Any 32 bytes serve as a label: this test is about encodings, and the one + * object here that would need an outer session is built by hand below. */ + for (k = 0; k < N; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k], &shares[k], NULL, sid) == 1); + nonce_ptrs[k] = &nonces[k]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &aggnonce, + nonce_ptrs, MU, N, T, &group_pk) == 1); + + /* The internal aggregate is optional, since nothing in the API takes one + * back. Asking for it must not change the nonce that does get published. */ + { + secp256k1_musig_pubnonce without; + CHECK(secp256k1_iceberg_nonce_agg(CTX, &without, NULL, + nonce_ptrs, MU, N, T, &group_pk) == 1); + CHECK(secp256k1_memcmp_var(&without, &group_pubnonce, sizeof(without)) == 0); + } + + /* Round-trips. Comparing the opaque structs rather than re-serializing + * catches a parse that loses something the encoding did carry. */ + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, nonce67, &nonces[2]) == 1); + CHECK(nonce67[0] == 3); + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 1); + CHECK(secp256k1_memcmp_var(&parsed_nonce, &nonces[2], sizeof(parsed_nonce)) == 0); + + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, agg66, &aggnonce) == 1); + CHECK(secp256k1_iceberg_aggnonce_parse(CTX, &parsed_agg, agg66) == 1); + CHECK(secp256k1_memcmp_var(&parsed_agg, &aggnonce, sizeof(parsed_agg)) == 0); + + /* A signature share, built by hand from a known scalar rather than by + * signing, so this test needs no outer session. */ + { + unsigned char raw[33]; + memset(raw, 0, sizeof(raw)); + raw[0] = 4; + raw[32] = 0x2b; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &psig, raw) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, psig33, &psig) == 1); + CHECK(secp256k1_memcmp_var(psig33, raw, sizeof(raw)) == 0); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &parsed_psig, psig33) == 1); + CHECK(secp256k1_memcmp_var(&parsed_psig, &psig, sizeof(psig)) == 0); + } + + /* An index past the end of the group names nobody. Zero is worse than that + * and the two are worth separating: zero is where the secret lives. A set of + * contributions containing a node at 0 puts the interpolation on top of one + * of its own nodes, where the Lagrange weights are 1 for that node and 0 for + * every other, so whoever sent it would be handing the group its aggregate + * rather than contributing to it. These three comparisons are the only thing + * standing there. */ + nonce67[0] = 0; + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 0); + nonce67[0] = SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1; + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 0); + nonce67[0] = 3; + psig33[0] = 0; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &parsed_psig, psig33) == 0); + psig33[0] = 4; + + /* Not a point. Corrupting the x coordinate is no good as a test, since + * about half of all x values do have a point, so break the prefix byte, which + * is invalid for any x. */ + { + unsigned char prefix = nonce67[1]; + nonce67[1] = 0x04; + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 0); + nonce67[1] = prefix; + } + agg66[33] = 0x04; + CHECK(secp256k1_iceberg_aggnonce_parse(CTX, &parsed_agg, agg66) == 0); + + /* A signature share at or above the group order. Aggregation is linear, so + * one that wrapped would combine into a signature that merely fails to + * verify, and nothing says which share was wrong. */ + memset(&psig33[1], 0xff, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &parsed_psig, psig33) == 0); + + /* Infinity is representable, and must be, because the participants choose + * their own contributions and nothing stops them summing to it. */ + memset(agg66, 0, sizeof(agg66)); + CHECK(secp256k1_iceberg_aggnonce_parse(CTX, &parsed_agg, agg66) == 1); + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, agg66, &parsed_agg) == 1); + for (k = 0; k < sizeof(agg66); k++) { + CHECK(agg66[k] == 0); + } +} + +/* Rebuild one participant's serialized share from the group's seeds. + * + * The vectors carry the seeds once rather than every share, because each seed + * belongs to n-(t-1) of them. Participant k holds the seeds whose subset leaves + * it out, in the same rank order the dealer writes them. */ +static size_t iceberg_test_share_from_seeds(unsigned char *out, const struct iceberg_session_vector *v, unsigned int k) { + size_t used = 4, rank; + + out[0] = 1; + out[1] = (unsigned char)v->n; + out[2] = (unsigned char)v->t; + out[3] = (unsigned char)k; + for (rank = 0; rank < v->n_seeds; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(v->n, v->t - 1, (uint32_t)rank); + if (subset & (secp256k1_rss_subset)(1u << k)) { + continue; + } + memcpy(&out[used], v->seeds[rank], 32); + used += 32; + } + CHECK(used == 4 + 32 * secp256k1_rss_binom(v->n - 1, v->t - 1)); + return used; +} + +/* Everything the group does, against what the Python reference produced. + * + * Until this existed the signing path was only ever checked against itself, so + * a systematic disagreement with the reference would have passed every other + * test in this file. It is not an authority, since both implementations + * could be wrong the same way, but it is what catches them drifting apart. */ +static void run_iceberg_vectors_test(void) { + size_t which; + + for (which = 0; which < sizeof(iceberg_session_vectors) / sizeof(iceberg_session_vectors[0]); which++) { + const struct iceberg_session_vector *v = &iceberg_session_vectors[which]; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + 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_musig_pubnonce group_pubnonce, cosigner_pubnonce; + const secp256k1_musig_pubnonce *just_the_cosigner[1]; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_partial_sig group_psig; + secp256k1_pubkey group_pk, cosigner_pk, parsed_group_pk; + const secp256k1_pubkey *pubkeys[2]; + unsigned char buf[4 + 32 * SECP256K1_ICEBERG_MAX_SEEDS]; + unsigned char wire[67], sid[32]; + unsigned int k; + size_t len; + + if (!iceberg_test_fits(v->n)) { + continue; + } + + for (k = 1; k <= v->n; k++) { + len = iceberg_test_share_from_seeds(buf, v, k); + CHECK(secp256k1_iceberg_share_parse(CTX, &shares[k - 1], buf, len) == 1); + } + + /* Public shares. Each one is an independent evaluation of the keygen + * label, so a disagreement here is in the PRF, the Lagrange weights or + * the seed layout, before any of the session machinery runs. */ + for (k = 1; k <= v->n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + CHECK(secp256k1_iceberg_pubshare_serialize(CTX, wire, &pubshares[k - 1]) == 1); + CHECK(wire[0] == k); + CHECK(secp256k1_memcmp_var(&wire[1], v->pubshares[k - 1], 33) == 0); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, v->mu, v->n, v->t) == 1); + + CHECK(secp256k1_ec_pubkey_parse(CTX, &parsed_group_pk, v->group_pk, 33) == 1); + CHECK(secp256k1_memcmp_var(&group_pk, &parsed_group_pk, sizeof(group_pk)) == 0); + + /* The outer session, whose key and nonce the vector fixes. */ + CHECK(secp256k1_ec_pubkey_parse(CTX, &cosigner_pk, v->cosigner_pk, 33) == 1); + pubkeys[0] = &group_pk; + pubkeys[1] = &cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, NULL, &keyagg_cache, pubkeys, 2) == 1); + CHECK(secp256k1_musig_pubnonce_parse(CTX, &cosigner_pubnonce, v->cosigner_pubnonce) == 1); + just_the_cosigner[0] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &cosigner_aggnonce, just_the_cosigner, 1) == 1); + + /* The label is an input, so the vector carries it. Any 32 bytes would + * do; these are the ones the generator happened to choose. */ + memcpy(sid, v->sid, 32); + + /* Round one. */ + for (k = 1; k <= v->mu; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, wire, &nonces[k - 1]) == 1); + CHECK(wire[0] == k); + CHECK(secp256k1_memcmp_var(&wire[1], v->pubnonces[k - 1], 66) == 0); + nonce_ptrs[k - 1] = &nonces[k - 1]; + } + + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &aggnonce, + nonce_ptrs, v->mu, v->n, v->t, &group_pk) == 1); + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, wire, &aggnonce) == 1); + CHECK(secp256k1_memcmp_var(wire, v->group_aggnonce, 66) == 0); + /* The published nonce differs from the internal one by b1: the + * aggregate compared above does not cover the nesting coefficient and + * this does. So do the signature shares below, by a different route. */ + CHECK(secp256k1_musig_pubnonce_serialize(CTX, wire, &group_pubnonce) == 1); + CHECK(secp256k1_memcmp_var(wire, v->group_pubnonce, 66) == 0); + + /* Round two. */ + for (k = 1; k <= v->t; k++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[k - 1], &shares[k - 1], NULL, sid, nonce_ptrs, v->mu, &group_pk, &keyagg_cache, v->msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, wire, &psigs[k - 1]) == 1); + CHECK(wire[0] == k); + CHECK(secp256k1_memcmp_var(&wire[1], v->psigs[k - 1], 32) == 0); + psig_ptrs[k - 1] = &psigs[k - 1]; + + /* Verify the vector's share, parsed back off the wire, rather than + * the one this run just produced. Everywhere else the verifier is + * checked against another run of the code that wrote it. */ + { + secp256k1_iceberg_partial_sig from_wire; + unsigned char in33[33]; + + in33[0] = (unsigned char)k; + memcpy(&in33[1], v->psigs[k - 1], 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &from_wire, in33) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &from_wire, &pubshares[k - 1], + nonce_ptrs, v->mu, v->n, v->t, &group_pk, &keyagg_cache, + v->msg, &cosigner_aggnonce) == 1); + } + } + + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, v->t, v->n, v->t) == 1); + CHECK(secp256k1_musig_partial_sig_serialize(CTX, wire, &group_psig) == 1); + CHECK(secp256k1_memcmp_var(wire, v->group_psig, 32) == 0); + } +} + +/* A participant who publishes something other than its real commitment raises + * the degree of the interpolant and is caught. */ +static void run_iceberg_keygen_cheat_test(void) { + secp256k1_iceberg_share shares[5]; + secp256k1_iceberg_share *ptrs[5]; + secp256k1_iceberg_pubshare pubshares[5]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[5]; + secp256k1_pubkey group_pk; + unsigned char seed[32]; + unsigned int k; + + testrand256(seed); + for (k = 0; k < 5; k++) { + ptrs[k] = &shares[k]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, ptrs, 5, 3, seed) == 1); + for (k = 0; k < 5; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k], &shares[k], NULL) == 1); + pubshare_ptrs[k] = &pubshares[k]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 5, 5, 3) == 1); + + /* Swap two participants' commitments: each is individually valid, but they + * no longer lie on one polynomial. */ + { + secp256k1_iceberg_pubshare swapped = pubshares[1]; + pubshares[1] = pubshares[3]; + pubshares[3] = swapped; + /* Restore the indices so only the points are wrong. */ + pubshares[1].data[4] = 2; + pubshares[3].data[4] = 4; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 5, 5, 3) == 0); + + /* Duplicated indices are refused rather than producing a singular system. */ + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[1], &shares[1], NULL) == 1); + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[3], &shares[3], NULL) == 1); + pubshares[3].data[4] = 2; + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 5, 5, 3) == 0); +} + +/* The three parsers, against input nobody meant them to see. + * + * The existing wire test picks malformations by hand: index zero, index past + * the maximum, a point off the curve, a scalar at the group order. Those are the + * cases somebody thought of. This covers the space between them: random buffers, + * and single-bit flips of valid encodings, which is where a parser that is + * almost right tends to be wrong. + * + * The invariant is round-tripping rather than acceptance. A parser is free to + * reject anything it likes; what it must not do is accept something it cannot + * then re-emit, because that is a value the rest of the module will treat as + * well-formed and the wire will not agree about. + */ +static void run_iceberg_parser_fuzz_test(void) { + secp256k1_iceberg_share shares[3]; + secp256k1_iceberg_share *share_ptrs[3]; + secp256k1_iceberg_pubnonce pubnonce, reparsed_nonce; + secp256k1_iceberg_aggnonce aggnonce, reparsed_agg; + secp256k1_iceberg_partial_sig psig, reparsed_psig; + secp256k1_musig_pubnonce musig_pubnonce; + const secp256k1_iceberg_pubnonce *nonce_ptrs[3]; + secp256k1_iceberg_pubshare pubshares[3]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[3]; + secp256k1_pubkey group_pk; + unsigned char seed[32], sid[32], seen[3][67], buffer[67], again[67]; + unsigned int k; + int i, round; + + testrand256(seed); + testrand256(sid); + for (k = 0; k < 3; k++) { + share_ptrs[k] = &shares[k]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, 3, 2, seed) == 1); + for (k = 0; k < 3; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k], &shares[k], NULL) == 1); + pubshare_ptrs[k] = &pubshares[k]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 3, 3, 2) == 1); + + /* One real encoding of each, to flip bits in. */ + CHECK(secp256k1_iceberg_nonce_gen(CTX, &pubnonce, &shares[0], NULL, sid) == 1); + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, seen[0], &pubnonce) == 1); + + /* Three distinct objects: nonce_agg reads each pointer in turn, so three + * pointers at one object would offer the same participant's index three + * times and be refused as duplicates. */ + { + secp256k1_iceberg_pubnonce nonces[3]; + for (k = 0; k < 3; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k], &shares[k], NULL, sid) == 1); + nonce_ptrs[k] = &nonces[k]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, &aggnonce, + nonce_ptrs, 3, 3, 2, &group_pk) == 1); + } + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, seen[1], &aggnonce) == 1); + memset(&psig, 0, sizeof(psig)); + { + /* A signature share built by hand, so this test does not need a whole + * session to have something well-formed to corrupt. */ + unsigned char raw[33]; + memset(raw, 0, sizeof(raw)); + raw[0] = 1; + /* Just under the group order, so a flipped bit in the top half has a + * chance of pushing it over rather than always landing in range. */ + raw[1] = 0xff; raw[2] = 0xff; raw[3] = 0xff; raw[4] = 0xff; + raw[32] = 7; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &psig, raw) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, seen[2], &psig) == 1); + } + + /* Random bytes will not find the scalar boundary: a random 32-byte value + * exceeds the group order with probability about 2^-128, so it essentially + * never overflows, and neither does a bit flipped in a small one. The out-of-range share is + * therefore named explicitly below. Without it a parser that reduced + * instead of rejecting passes everything above. */ + { + static const unsigned char order[32] = { + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe, + 0xba,0xae,0xdc,0xe6,0xaf,0x48,0xa0,0x3b,0xbf,0xd2,0x5e,0x8c,0xd0,0x36,0x41,0x41 + }; + unsigned char probe[33]; + probe[0] = 1; + + memcpy(&probe[1], order, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 0); + + memset(&probe[1], 0xff, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 0); + + /* One below the order is the largest thing that must be accepted, and it + * has to survive a round trip unchanged rather than being reduced. */ + memcpy(&probe[1], order, 32); + probe[32] -= 1; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, again, &reparsed_psig) == 1); + CHECK(secp256k1_memcmp_var(again, probe, 33) == 0); + + /* And zero, which is in range and easy to special-case by accident. */ + memset(&probe[1], 0, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, again, &reparsed_psig) == 1); + CHECK(secp256k1_memcmp_var(again, probe, 33) == 0); + } + + /* round 0: random bytes. round 1: one bit flipped in something valid. */ + for (round = 0; round < 2; round++) { + for (i = 0; i < 64 * COUNT; i++) { + int which = (int)testrand_int(3); + size_t length = which == 0 ? 67 : (which == 1 ? 66 : 33); + + if (round == 0) { + testrand_bytes_test(buffer, length); + } else { + memcpy(buffer, seen[which], length); + testrand_flip(buffer, length); + } + + if (which == 0) { + if (secp256k1_iceberg_pubnonce_parse(CTX, &reparsed_nonce, buffer)) { + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, again, &reparsed_nonce) == 1); + CHECK(secp256k1_memcmp_var(again, buffer, length) == 0); + } + } else if (which == 1) { + if (secp256k1_iceberg_aggnonce_parse(CTX, &reparsed_agg, buffer)) { + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, again, &reparsed_agg) == 1); + CHECK(secp256k1_memcmp_var(again, buffer, length) == 0); + } + } else { + if (secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, buffer)) { + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, again, &reparsed_psig) == 1); + CHECK(secp256k1_memcmp_var(again, buffer, length) == 0); + } + } + } + } +} + +/* Constraint C1, exhaustively: whoever shows up, the signature is the same one. + * + * The scheme's whole reason for deriving nonces rather than storing them is that + * a member can go offline between the rounds and rejoin. So round one may be run + * by any 2t-1 members, round two signed by any t members, and the two sets need + * not overlap at all, and every combination must produce a byte-identical + * signature, because the aggregate nonce and the key are the same either way. + * + * The e2e test checks two of those combinations. This checks all of them, at + * sizes where "all of them" is cheap, because an earlier version of this module + * imposed a restriction that broke C1 and no test could see it: every + * configuration then had n == 2t-1, so there was only ever one round-one set. + * + * The cosigner's nonce is regenerated from the same randomness inside the loop. + * That is not laziness. The final signature depends on it, so holding it fixed + * is what makes "byte-identical" the right assertion rather than a coincidence. + */ +static void run_iceberg_c1_property_test(void) { + static const unsigned char configs[][2] = { {4,2}, {5,2} }; + size_t config; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1], mu = 2 * t - 1; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + 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_pubkey group_pk, cosigner_pk; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + unsigned char seed[32], msg[32], seckey[32], secrand[32], sid[32]; + unsigned char first_sig[64]; + unsigned char round_one[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + /* musig_nonce_gen wipes the randomness it is handed, so that one call + * cannot silently become two. Keep a master copy and spend a fresh one + * each time round the loop. */ + unsigned char secrand_master[32]; + unsigned char signers[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned int k; + size_t i; + int have_first = 0; + int combinations = 0; + + if (!iceberg_test_fits(n)) { + continue; + } + + testrand256(seed); + testrand256(msg); + testrand256(seckey); + testrand256(secrand_master); + testrand256(sid); + + for (k = 1; k <= n; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, n, t, seed) == 1); + + for (k = 1; k <= n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, mu, n, t) == 1); + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + + /* Every set that could run round one. */ + for (i = 0; i < mu; i++) { + round_one[i] = (unsigned char)(i + 1); + } + do { + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + const secp256k1_musig_pubnonce *all_pubnonces[2]; + secp256k1_musig_aggnonce cosigner_aggnonce, full_aggnonce; + + for (i = 0; i < mu; i++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[i], + &shares[round_one[i] - 1], NULL, sid) == 1); + nonce_ptrs[i] = &nonces[i]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + nonce_ptrs, mu, n, t, &group_pk) == 1); + + /* Every set that could then sign, drawn from the whole group. */ + for (i = 0; i < t; i++) { + signers[i] = (unsigned char)(i + 1); + } + do { + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_session session; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + const secp256k1_musig_partial_sig *musig_psigs[2]; + unsigned char sig[64]; + + /* Rebuilt every time, from the same randomness, so the only + * thing varying across the loop is who took part. */ + memcpy(secrand, secrand_master, sizeof(secrand)); + CHECK(secp256k1_musig_nonce_gen(CTX, &cosigner_secnonce, &cosigner_pubnonce, + secrand, seckey, &cosigner_pk, msg, + &keyagg_cache, NULL) == 1); + all_pubnonces[0] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &cosigner_aggnonce, all_pubnonces, 1) == 1); + all_pubnonces[0] = &group_pubnonce; + all_pubnonces[1] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &full_aggnonce, all_pubnonces, 2) == 1); + CHECK(secp256k1_musig_nonce_process(CTX, &session, &full_aggnonce, + msg, &keyagg_cache, NULL) == 1); + + for (i = 0; i < t; i++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[signers[i] - 1], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &cosigner_secnonce, + &cosigner_keypair, &keyagg_cache, + &session) == 1); + musig_psigs[0] = &group_psig; + musig_psigs[1] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig, &session, musig_psigs, 2) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &agg_xonly) == 1); + + if (!have_first) { + memcpy(first_sig, sig, sizeof(sig)); + have_first = 1; + } else { + CHECK(secp256k1_memcmp_var(sig, first_sig, sizeof(sig)) == 0); + } + combinations++; + } while (iceberg_test_next_combination(signers, t, n)); + } while (iceberg_test_next_combination(round_one, mu, n)); + + /* C(n, mu) * C(n, t), so a silently truncated loop is caught. */ + CHECK(combinations == (int)(secp256k1_rss_binom(n, mu) * secp256k1_rss_binom(n, t))); + } +} + +/* A whole signing session: an Iceberg group standing in for one MuSig2 + * participant, beside an ordinary single signer who never learns a group is + * involved, ending in a BIP-340 signature. + * + * The cosigner produces its nonce first here, but only because something has to + * be written first. Both sides of round one need the session label and nothing + * from each other, so either order works and a group can open a session. + */ +static void run_iceberg_e2e_test_internal(void) { + /* The last two have n > 2t-1, so round one leaves somebody out. That is + * what reaches the branch below where a member absent from round one signs + * anyway; with n == 2t-1 throughout, nothing exercises it. */ + static const unsigned char configs[][2] = { {3,2}, {5,3}, {7,4}, {5,2}, {7,3} }; + size_t config; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1], mu = 2 * t - 1; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share_cache first_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 iceberg_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; + const secp256k1_musig_pubnonce *all_pubnonces[2]; + secp256k1_musig_aggnonce cosigner_aggnonce, full_aggnonce; + secp256k1_musig_session session; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + const secp256k1_musig_partial_sig *musig_psigs[2]; + + unsigned char seed[32], msg[32], seckey[32], secrand[32], sid[32]; + unsigned char sig[64], sig_again[64]; + unsigned int k; + size_t i; + + testrand256(seed); + testrand256(msg); + testrand256(seckey); + if (!iceberg_test_fits(n)) { + continue; + } + + testrand256(secrand); + + /** setup **/ + for (k = 1; k <= n; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, n, t, seed) == 1); + + for (k = 1; k <= n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, mu, n, t) == 1); + + /* The group's key enters aggregation as an ordinary public key. */ + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + + /** round 1 **/ + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + + /* Any 32 bytes. The caller owns this choice and the rule about it. */ + memset(sid, 0x5b, sizeof(sid)); + + for (k = 1; k <= n; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + nonce_ptrs[k - 1] = &nonces[k - 1]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &iceberg_aggnonce, + nonce_ptrs, mu, n, t, &group_pk) == 1); + + all_pubnonces[0] = &group_pubnonce; + all_pubnonces[1] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &full_aggnonce, all_pubnonces, 2) == 1); + CHECK(secp256k1_musig_nonce_process(CTX, &session, &full_aggnonce, msg, &keyagg_cache, NULL) == 1); + + /** round 2 **/ + for (k = 1; k <= t; k++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[k - 1], &shares[k - 1], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[k - 1] = &psigs[k - 1]; + } + /* The cache must not change a single answer. It only saves recomputing + * the Lagrange weights, so the whole risk in it is that it silently + * produces a different one, and every other call in this file passes NULL, + * which would leave that undetected. Both rounds, byte for byte. */ + CHECK(secp256k1_iceberg_share_cache_create(CTX, &first_cache, &shares[0]) == 1); + { + secp256k1_iceberg_pubnonce cached_nonce; + secp256k1_iceberg_partial_sig cached_psig; + CHECK(secp256k1_iceberg_nonce_gen(CTX, &cached_nonce, &shares[0], &first_cache, sid) == 1); + CHECK(secp256k1_memcmp_var(&cached_nonce, &nonces[0], sizeof(cached_nonce)) == 0); + CHECK(secp256k1_iceberg_partial_sign(CTX, &cached_psig, &shares[0], &first_cache, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_memcmp_var(&cached_psig, &psigs[0], sizeof(cached_psig)) == 0); + } + + /* Every share verifies against its author's public share, at every + * configuration this test runs. */ + for (k = 1; k <= t; k++) { + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[k - 1], &pubshares[k - 1], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 1); + } + { + secp256k1_iceberg_partial_sig altered = psigs[0]; + unsigned char other_msg[32]; + + altered.data[10] ^= 0x40; + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &altered, &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + + /* Participant 2's share against participant 1's public share. */ + if (t >= 2) { + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[1], &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + } + + /* Participant 1's own share, relabeled as + * participant 2's, against participant 1's public share. Everything + * the equation touches is participant 1's, so it still balances, + * and nothing but the index comparison rejects this. Without that + * comparison the answer would be a confident yes to a question + * about participant 2. */ + altered = psigs[0]; + altered.data[4] = 2; + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &altered, &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + + /* An input that differs from the signer's gives the same 0 as a bad + * share, which is the reason a 0 is not evidence about a member. */ + memcpy(other_msg, msg, 32); + other_msg[0] ^= 1; + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, other_msg, + &cosigner_aggnonce) == 0); + + /* An uninitialized share is a 0 rather than a crash. */ + memset(&altered, 0, sizeof(altered)); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &altered, &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + } + + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + + /* One share past the threshold, which is where aggregation can tell that + * a set contradicts itself. At exactly t it cannot: t points always lie + * on some polynomial of degree t-1, so the tampered share below is + * accepted there and refused here, and that difference is the check. */ + if (t + 1 <= n) { + secp256k1_musig_partial_sig with_spare; + secp256k1_iceberg_partial_sig tampered; + + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[t], &shares[t], NULL, sid, + nonce_ptrs, mu, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + psig_ptrs[t] = &psigs[t]; + + /* A spare share changes nothing: any qualifying set interpolates to + * the same value, which is the property the whole scheme rests on. */ + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &with_spare, psig_ptrs, t + 1, n, t) == 1); + CHECK(secp256k1_memcmp_var(&with_spare, &group_psig, sizeof(with_spare)) == 0); + + tampered = psigs[0]; + tampered.data[36] ^= 1; + psig_ptrs[0] = &tampered; + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &with_spare, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &with_spare, psig_ptrs, t + 1, n, t) == 0); + psig_ptrs[0] = &psigs[0]; + } + + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &cosigner_secnonce, + &cosigner_keypair, &keyagg_cache, &session) == 1); + + musig_psigs[0] = &group_psig; + musig_psigs[1] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig, &session, musig_psigs, 2) == 1); + + /* The whole point: an ordinary BIP-340 signature. */ + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &agg_xonly) == 1); + + /* A different quorum in round two produces the identical signature. + * The signers may be any t of the n. A member absent from round one + * still holds the share that fixes what its contribution would have + * been, so it can check the set it is handed and sign against it. The + * case below with mu < n exercises exactly that. */ + if (mu > t) { + for (i = 0; i < t; i++) { + k = (unsigned int)(mu - t + i + 1); + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[k - 1], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig_again, &session, musig_psigs, 2) == 1); + CHECK(secp256k1_memcmp_var(sig, sig_again, 64) == 0); + CHECK(secp256k1_schnorrsig_verify(CTX, sig_again, msg, 32, &agg_xonly) == 1); + } + + /* What the module enforces, and what it does not. + * + * It does NOT refuse a different message under the same label. The + * label is an argument again, because Lightning fixes the nonce a full + * round-trip before the transaction exists and a label derived from the + * message could not be computed in time. So one label with two messages + * is expressible, it is the first attack in this family, and it is the + * caller's job to make it impossible: half by never answering twice + * under one label, half by the group agreeing which message a label + * belongs to. See doc/iceberg.md. + * + * What it does enforce is that the contributions it is handed belong to + * the session it thinks it is in. */ + { + /* A set from a different label is refused. The contributions are + * internally consistent, since they are a real sharing, but the + * polynomial they define does not pass through this participant's + * own contribution for the label it is signing under, and that is + * what the check compares. */ + secp256k1_iceberg_pubnonce elsewhere[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubnonce *elsewhere_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned char other_sid[32]; + memcpy(other_sid, sid, 32); + other_sid[0] ^= 1; + for (i = 0; i < mu; i++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &elsewhere[i], &shares[i], NULL, other_sid) == 1); + elsewhere_ptrs[i] = &elsewhere[i]; + } + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, elsewhere_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 0); + + /* And the property that check must not cost: a member who was + * absent from round one can still sign. Its contribution is a + * function of its share and the label, so it can derive what its + * contribution would have been and compare that against the + * polynomial, without having been one of the contributors. + * + * Requiring the member's own contribution to be present would break + * this and buy nothing: a corrupt minority can fit a consistent set + * through any single honest contribution either way. Only checkable + * when the group is larger than the quorum. */ + if (n > mu) { + const secp256k1_iceberg_pubnonce *without_us[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + for (i = 0; i < mu; i++) { + without_us[i] = nonce_ptrs[i + 1]; + } + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, without_us, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[mu], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + + /* And that share verifies, which is the case the verifier exists + * for: participant mu+1 published no contribution, so + * there is no nonce of its own to hand over. The set fixes what + * its nonce had to be, and the verifier reads it from there. */ + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[mu], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 1); + } + + /* The count a caller gets wrong: t contributions instead of the + * 2t-1 this needs, because t is the number that signs and it is + * easy to assume it is also the number to pass. Returns 0 rather + * than aborting, which is what makes it worth documenting as + * recoverable. */ + if (mu > t) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, nonce_ptrs, t, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 0); + } + + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + for (i = 1; i < t; i++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[i], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + psig_ptrs[0] = &psigs[0]; + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig_again, &session, musig_psigs, 2) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig_again, msg, 32, &agg_xonly) == 1); + } + } +} + +/* Iceberg under tweaks, which every Taproot output uses. + * + * `tacc` belongs to the outer session: musig_nonce_process sets `e * tacc` + * aside and musig_partial_sig_agg adds it once. A group's shares therefore + * carry `e * a * g * gacc` and NOT the tweak, or it is added twice and the + * signature fails. Nothing in the module mentions tacc, so the correct + * behavior is an absence, and this is what would catch someone reintroducing + * it while editing the key coefficient. + * + * Both tweak kinds and both orders, since tacc accumulates and an x-only tweak + * can flip the aggregate key's parity. */ +static void run_iceberg_tweak_test_internal(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + /* ec then xonly, xonly then ec, two xonly, and one of each alone. */ + static const unsigned char plans[][2] = { {0,1}, {1,0}, {1,1}, {0,2}, {1,2} }; + size_t plan; + + if (!iceberg_test_fits(N)) { + return; + } + + for (plan = 0; plan < sizeof(plans) / sizeof(plans[0]); plan++) { + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N]; + secp256k1_iceberg_pubnonce nonces[N]; + const secp256k1_iceberg_pubnonce *nonce_ptrs[N]; + secp256k1_iceberg_partial_sig psigs[N]; + const secp256k1_iceberg_partial_sig *psig_ptrs[N]; + secp256k1_iceberg_aggnonce iceberg_aggnonce; + + secp256k1_pubkey group_pk, cosigner_pk, output_pk; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly, output_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + const secp256k1_musig_pubnonce *all_pubnonces[2]; + secp256k1_musig_aggnonce cosigner_aggnonce, full_aggnonce; + secp256k1_musig_session session; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + const secp256k1_musig_partial_sig *musig_psigs[2]; + + unsigned char seed[32], msg[32], seckey[32], secrand[32], sid[32]; + unsigned char tweak[32], sig[64]; + unsigned int k, step; + + testrand256(seed); + testrand256(msg); + testrand256(seckey); + testrand256(secrand); + + for (k = 1; k <= N; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + + /* Tweak the OUTER aggregate, which is where a Taproot output comes + * from. The group never learns it happened: nothing below this point + * passes the tweak to an Iceberg call. */ + for (step = 0; step < 2; step++) { + unsigned char kind = plans[plan][step]; + if (kind == 2) { + continue; /* this plan applies only one tweak */ + } + testrand256(tweak); + if (kind == 0) { + CHECK(secp256k1_musig_pubkey_ec_tweak_add(CTX, &output_pk, &keyagg_cache, tweak) == 1); + } else { + CHECK(secp256k1_musig_pubkey_xonly_tweak_add(CTX, &output_pk, &keyagg_cache, tweak) == 1); + } + } + CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &output_xonly, NULL, &output_pk) == 1); + + /** round 1, exactly as untweaked **/ + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + memset(sid, 0x3e, sizeof(sid)); + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + nonce_ptrs[k - 1] = &nonces[k - 1]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &iceberg_aggnonce, + nonce_ptrs, MU, N, T, &group_pk) == 1); + all_pubnonces[0] = &group_pubnonce; + all_pubnonces[1] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &full_aggnonce, all_pubnonces, 2) == 1); + CHECK(secp256k1_musig_nonce_process(CTX, &session, &full_aggnonce, msg, &keyagg_cache, NULL) == 1); + + /** round 2 **/ + for (k = 1; k <= T; k++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[k - 1], &shares[k - 1], NULL, sid, nonce_ptrs, MU, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[k - 1] = &psigs[k - 1]; + } + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, T, N, T) == 1); + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &cosigner_secnonce, + &cosigner_keypair, &keyagg_cache, &session) == 1); + musig_psigs[0] = &group_psig; + musig_psigs[1] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig, &session, musig_psigs, 2) == 1); + + /* Verifies under the TWEAKED key, and not under the untweaked one -- + * the second half matters, because a signature that verified under both + * would mean the tweak had not reached the challenge at all. */ + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &output_xonly) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &agg_xonly) == 0); + } +} + +REPEAT_TEST(run_iceberg_tweak_test) +REPEAT_TEST(run_iceberg_e2e_test) +REPEAT_TEST(run_iceberg_interpolate_test) +REPEAT_TEST(run_iceberg_batch_inverse_test) +REPEAT_TEST(run_iceberg_pss_test) +REPEAT_TEST(run_iceberg_vpss_test) +REPEAT_TEST(run_iceberg_vpss_quorum_test) +REPEAT_TEST(run_iceberg_keygen_test) + +/* The dealer's output, pinned to a fixed answer. + * + * Nothing else pins it. run_iceberg_vectors_test never calls shares_gen; it + * rebuilds shares from the per-subset seeds the vectors already carry, and + * every other test is self-consistent, so a change to the derivation moves all + * of them together and none of them notices. Without this test, changing the t + * byte of the dealer's preimage to t + 1 is caught by nothing. + * + * The digest is over the five serialized shares of one deal, in participant + * order. Regenerating it after a deliberate change is easy and almost always the + * wrong thing to do: a serialized share is a wire format two implementations + * have to agree on byte for byte, so a change here is a change to that format + * and the reference implementation has to move with it. */ +static void run_iceberg_dealer_kat_test(void) { + enum { N = 5, T = 3 }; + static const unsigned char expected[32] = { + 0x74, 0xf0, 0xec, 0x3f, 0xec, 0xbf, 0x47, 0xae, + 0x81, 0xbf, 0xec, 0x45, 0xdd, 0x01, 0xe2, 0x12, + 0xf5, 0x30, 0xd1, 0x1c, 0xf4, 0x7e, 0x89, 0x1a, + 0x7d, 0x19, 0xb5, 0x14, 0x71, 0xb8, 0xed, 0x09 + }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *ptrs[N]; + secp256k1_sha256 sha; + unsigned char seed[32], buf[SECP256K1_ICEBERG_SHARE_MAX_LEN], digest[32]; + unsigned int k; + size_t len; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0xa7, sizeof(seed)); + for (k = 1; k <= N; k++) { + ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, ptrs, N, T, seed) == 1); + + secp256k1_sha256_initialize(&sha); + for (k = 1; k <= N; k++) { + len = sizeof(buf); + CHECK(secp256k1_iceberg_share_serialize(CTX, buf, &len, &shares[k - 1]) == 1); + CHECK(len == 4 + 32 * secp256k1_rss_binom(N - 1, T - 1)); + /* The header gets its own assertion: a wrong version, group, threshold + * or index gives a clearer failure here than a digest ever will. */ + CHECK(buf[0] == 1 && buf[1] == N && buf[2] == T && buf[3] == k); + secp256k1_sha256_write(secp256k1_get_hash_context(CTX), &sha, buf, len); + } + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &sha, digest); + CHECK(secp256k1_memcmp_var(digest, expected, sizeof(digest)) == 0); +} + +/* An index the group does not contain must be refused by whatever knows the + * group size. + * + * Each check must bound against the group's own n, not the compile-time + * maximum. The parsers let the band n < k <= 10 through deliberately, having no + * way to know n, which leaves these calls as the only place it can be stopped. + * + * One group of ten is dealt and then five of its members, one of them + * participant 7, are presented to calls told the group is five. Dealing a single + * group is what makes this a test of the index check and not of the degree + * check: every point really does lie on the same degree t-1 polynomial, so a + * build that bounds by the maximum finds nothing else to object to and hands + * back the ten-member group's key to a caller who believes it has five. + * + * Each rejection is paired with the same call at n = 10, which must succeed. A + * test that only checks for 0 cannot tell a working bound from a broken set. */ +static void run_iceberg_foreign_index_test(void) { + enum { N = 10, T = 3, MU = 2 * T - 1, CLAIMED = 5 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *quorum_pubshares[MU]; + secp256k1_iceberg_pubnonce nonces[N]; + const secp256k1_iceberg_pubnonce *quorum_nonces[MU], *inside_nonces[MU]; + secp256k1_iceberg_partial_sig psigs[T]; + const secp256k1_iceberg_partial_sig *psig_ptrs[T]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_partial_sig musig_psig; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_pubkey group_pk, again, cosigner_pk; + unsigned char seed[32], seckey[32], secrand[32], sid[32], msg[32]; + /* Four members the claimed group of five contains, and one it does not. */ + const unsigned int members[MU] = { 1, 2, 3, 4, 7 }; + /* The t who go on to sign, participant 7 among them. */ + const unsigned int signers[T] = { 1, 2, 7 }; + /* A second quorum, every member of which the claimed group does contain. */ + const unsigned int inside[MU] = { 1, 2, 3, 4, 5 }; + unsigned int k; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x41, sizeof(seed)); + memset(seckey, 0x31, sizeof(seckey)); + memset(secrand, 0x77, sizeof(secrand)); + memset(sid, 0x53, sizeof(sid)); + memset(msg, 0x64, sizeof(msg)); + + for (k = 1; k <= N; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + } + for (i = 0; i < MU; i++) { + quorum_pubshares[i] = &pubshares[members[i] - 1]; + } + + /* Aggregation. The same five public shares, the same degree check. */ + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, quorum_pubshares, MU, N, T) == 1); + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &again, quorum_pubshares, MU, CLAIMED, T) == 0); + + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + } + for (i = 0; i < MU; i++) { + quorum_nonces[i] = &nonces[members[i] - 1]; + inside_nonces[i] = &nonces[inside[i] - 1]; + } + + /* Round one. */ + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + quorum_nonces, MU, N, T, &group_pk) == 1); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + quorum_nonces, MU, CLAIMED, T, &group_pk) == 0); + + for (i = 0; i < T; i++) { + k = signers[i]; + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[k - 1], NULL, sid, + quorum_nonces, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + + /* Verification refuses twice over, and for two different reasons: first on + * an index inside the set of contributions, then, with a quorum the claimed + * group does contain, on the public share's own index. */ + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[signers[0] - 1], + quorum_nonces, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[signers[0] - 1], + quorum_nonces, MU, CLAIMED, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 0); + /* And participant 7's own share, checked against participant 7, over a + * quorum the claimed group does contain, so that the contribution indices + * cannot be what rejects it and the public share's own index is left as the + * only thing that can. Participant 7 can sign over a set it is absent from: + * that is the property partial_sign is built around. */ + { + secp256k1_iceberg_partial_sig seven; + CHECK(secp256k1_iceberg_partial_sign(CTX, &seven, &shares[6], NULL, sid, + inside_nonces, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &seven, &pubshares[6], + inside_nonces, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &seven, &pubshares[6], + inside_nonces, MU, CLAIMED, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 0); + } + + /* And aggregation of the signature shares, the last place it could get in. */ + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T, N, T) == 1); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T, CLAIMED, T) == 0); + + /* An index can also be wrong by being repeated, which makes the + * interpolation singular and is how a caller most easily passes fewer + * distinct participants than it believes it has. Every call that walks a set + * of indices has to catch it; only pubkey_agg was tested for it. */ + { + const secp256k1_iceberg_pubnonce *repeated[MU]; + const secp256k1_iceberg_partial_sig *repeated_psigs[T]; + + for (i = 0; i < MU; i++) { + repeated[i] = quorum_nonces[i]; + } + repeated[MU - 1] = repeated[0]; + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + repeated, MU, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[signers[0] - 1], + repeated, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 0); + for (i = 0; i < T; i++) { + repeated_psigs[i] = psig_ptrs[i]; + } + repeated_psigs[T - 1] = repeated_psigs[0]; + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, repeated_psigs, T, N, T) == 0); + } +} + +/* Every contribution at infinity, which is the only way to reach nonce_agg's + * last refusal. The degree check passes, because the zero polynomial has every + * degree; the group's pair then comes out as the point at infinity, which a + * MuSig2 public nonce has no encoding for. + * + * Which makes this the place to say what a refused call leaves behind. Nothing + * that loads: the aggnonce keeps the zero tag from the memset at the top of the + * call, so the serializer refuses it rather than handing back an aggregate the + * caller never got a 1 for. */ +static void run_iceberg_infinity_nonce_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_pubnonce nonces[MU]; + const secp256k1_iceberg_pubnonce *ptrs[MU]; + secp256k1_iceberg_aggnonce refused; + secp256k1_musig_pubnonce group_pubnonce; + secp256k1_pubkey group_pk; + unsigned char in67[67], out66[66], seckey[32]; + size_t i; + + memset(seckey, 0x9d, sizeof(seckey)); + CHECK(secp256k1_ec_pubkey_create(CTX, &group_pk, seckey) == 1); + + /* An all-zero point is how the 33-byte encoding spells infinity, so these + * parse rather than being refused one at a time. */ + memset(in67, 0, sizeof(in67)); + for (i = 0; i < MU; i++) { + in67[0] = (unsigned char)(i + 1); + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &nonces[i], in67) == 1); + ptrs[i] = &nonces[i]; + } + + memset(&refused, 0xab, sizeof(refused)); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &refused, + ptrs, MU, N, T, &group_pk) == 0); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_aggnonce_serialize(CTX, out66, &refused)); +} + +/* Every documented bound that nothing else here reaches, at the severity the + * header documents for it. The distinction is the module's one departure from the + * library's convention and it is load-bearing: a count that arrives from a peer + * returns 0, and the group's own n and t abort, so a caller can tell "they sent + * me a bad set" from "I have a bug" without a debugger. + * + * The upper bounds are covered by run_iceberg_null_entry_test, which needs them + * to keep its own sweep unreachable. The lower bounds were covered nowhere, and + * neither was the static context, which three calls document and musig's own + * suite tests for its equivalents. */ +static void run_iceberg_bounds_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N], scratch_pubshare; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N + 1]; + secp256k1_iceberg_pubnonce pubnonces[N], scratch_pubnonce; + const secp256k1_iceberg_pubnonce *pubnonce_ptrs[N + 1]; + secp256k1_iceberg_partial_sig psigs[T], scratch_psig; + const secp256k1_iceberg_partial_sig *psig_ptrs[N + 1]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, musig_pubnonce; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_partial_sig musig_psig; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_pubkey group_pk, scratch_pk, cosigner_pk; + unsigned char seed[32], seckey[32], secrand[32], sid[32], msg[32]; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x5e, sizeof(seed)); + memset(seckey, 0x2b, sizeof(seckey)); + memset(secrand, 0x6f, sizeof(secrand)); + memset(sid, 0x3d, sizeof(sid)); + memset(msg, 0x71, sizeof(msg)); + for (i = 0; i < N; i++) { + share_ptrs[i] = &shares[i]; + pubshare_ptrs[i] = &pubshares[i]; + pubnonce_ptrs[i] = &pubnonces[i]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (i = 0; i < N; i++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[i], &shares[i], NULL) == 1); + CHECK(secp256k1_iceberg_nonce_gen(CTX, &pubnonces[i], &shares[i], NULL, sid) == 1); + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + for (i = 0; i < T; i++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[i], NULL, sid, + pubnonce_ptrs, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + /* The entry past the group is null in each array, which is what makes the + * count bound the reason for the refusal. A duplicate there would be caught + * by the repeated-index check instead, and the bound could be deleted with + * the suite still passing; a null one reaches the sweep and aborts. */ + pubshare_ptrs[N] = NULL; + pubnonce_ptrs[N] = NULL; + for (i = T; i < N + 1; i++) { + psig_ptrs[i] = NULL; + } + + /* How many contributions turned up is a fact about the group, so every call + * that takes a count returns 0 rather than aborting: too few of them, and + * more of them than the group holds, which is one contribution arriving + * twice. */ + CHECK(secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, sid, + pubnonce_ptrs, MU - 1, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], pubnonce_ptrs, + MU - 1, N, T, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, MU - 1, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, N + 1, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, MU - 1, N, T) == 0); + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, N + 1, N, T) == 0); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T - 1, N, T) == 0); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, N + 1, N, T) == 0); + + /* A threshold above (n+1)/2, which no group can have: 2t-1 would not fit. + * Every call that takes t checks it before the count, so each one can be + * asked in isolation. */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, + MU, N, (N + 1) / 2 + 1)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, MU, N, + (N + 1) / 2 + 1, &group_pk)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, + (N + 1) / 2 + 1, N, + (N + 1) / 2 + 1)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, (N + 1) / 2 + 1, seed)); + + /* The three calls whose Args line says "not secp256k1_context_static". Each + * multiplies by the generator, which that context has no table for. */ + CHECK_ILLEGAL(STATIC_CTX, secp256k1_iceberg_pubshare_gen(STATIC_CTX, &scratch_pubshare, + &shares[0], NULL)); + CHECK_ILLEGAL(STATIC_CTX, secp256k1_iceberg_nonce_gen(STATIC_CTX, &scratch_pubnonce, + &shares[0], NULL, sid)); + CHECK_ILLEGAL(STATIC_CTX, secp256k1_iceberg_partial_sign(STATIC_CTX, &scratch_psig, + &shares[0], NULL, sid, + pubnonce_ptrs, MU, &group_pk, + &keyagg_cache, msg, + &cosigner_aggnonce)); +} + +/* A null entry in a caller-supplied array. + * + * Every function here takes its contributions as an array of pointers, and a + * coordinator that assembles one from a map of who has answered can leave a + * hole in it. The sweep for null entries is the only thing that can object, so + * everything is built and left well formed before any entry is nulled: a + * zeroed object would be caught earlier by a magic-byte check inside a load + * helper instead. The failing calls write to scratch out-params because a call + * that fails still zeroes its own output first. */ +static void run_iceberg_null_entry_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N]; + secp256k1_iceberg_pubnonce pubnonces[N]; + const secp256k1_iceberg_pubnonce *pubnonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_partial_sig psigs[T], scratch_psig; + const secp256k1_iceberg_partial_sig *psig_ptrs[T]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, musig_pubnonce; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_partial_sig musig_psig; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_pubkey group_pk, scratch_pk, cosigner_pk; + const secp256k1_pubkey *pubkeys[2]; + unsigned char seed[32], seckey[32], secrand[32], sid[32], msg[32]; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x7c, sizeof(seed)); + memset(seckey, 0x31, sizeof(seckey)); + memset(secrand, 0x77, sizeof(secrand)); + memset(sid, 0x11, sizeof(sid)); + memset(msg, 0x22, sizeof(msg)); + for (i = 0; i < N; i++) { + share_ptrs[i] = &shares[i]; + pubshare_ptrs[i] = &pubshares[i]; + pubnonce_ptrs[i] = &pubnonces[i]; + } + /* Past the group, left null on purpose: a count that reaches them is a + * count that should have been refused before the sweep ran. */ + for (i = N; i < SECP256K1_ICEBERG_MAX_PARTICIPANTS; i++) { + pubnonce_ptrs[i] = NULL; + } + for (i = 0; i < T; i++) { + psig_ptrs[i] = &psigs[i]; + } + + /* The dealer sweeps before it writes, so the real deal below is unaffected. */ + share_ptrs[2] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed)); + share_ptrs[2] = &shares[2]; + + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (i = 0; i < N; i++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[i], &shares[i], NULL) == 1); + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + for (i = 0; i < N; i++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &pubnonces[i], &shares[i], NULL, sid) == 1); + } + for (i = 0; i < T; i++) { + /* nonce_agg belongs in this sweep too: its count bound is what keeps the + * loop below inside an array the caller sized from n. */ + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, pubnonce_ptrs, + N + 1, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[i], NULL, sid, + pubnonce_ptrs, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + } + + pubshare_ptrs[2] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, MU, N, T)); + pubshare_ptrs[2] = &pubshares[2]; + + /* keyagg_check walks a list too, and its own is the cosigners' rather than + * the group's, so it needs its own turn. */ + pubkeys[1] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk)); + pubkeys[1] = &cosigner_pk; + + pubnonce_ptrs[2] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, MU, N, T, &group_pk)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, + sid, pubnonce_ptrs, MU, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], + pubnonce_ptrs, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce)); + pubnonce_ptrs[2] = &pubnonces[2]; + + psig_ptrs[1] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T, N, T)); + psig_ptrs[1] = &psigs[1]; + + /* A count larger than the group is peer-influenced rather than a caller + * bug, so these return 0 instead. The distinction is what stops the sweep + * above from being reachable with a count it cannot walk. + * + * The first pair passes a count just past the group, with the entries past + * it null: that is the case the header's promise is about, and the one the + * sweep would otherwise reach first. The second pair passes a count past the + * compile-time bound too, which the same `> n` refuses: there is no + * separate check against MAX_PARTICIPANTS here, because n comes off the + * caller's own share and cannot exceed it. Both pairs are the one mechanism, + * approached from two distances. */ + CHECK(secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, sid, + pubnonce_ptrs, N + 1, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], pubnonce_ptrs, + N + 1, N, T, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, sid, + pubnonce_ptrs, SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1, + &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], pubnonce_ptrs, + SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1, N, T, + &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); +} + +static const struct tf_test_entry tests_iceberg[] = { + CASE1(run_iceberg_from_roots_test), + CASE1(run_iceberg_div_root_test), + CASE1(run_iceberg_lagrange_eval_test), + CASE1(run_iceberg_lagrange_basis_test), + CASE1(run_iceberg_interpolate_test), + CASE1(run_iceberg_batch_inverse_test), + CASE1(run_iceberg_binom_test), + CASE1(run_iceberg_subset_rank_test), + CASE1(run_iceberg_prf_test), + CASE1(run_iceberg_midstate_test), + CASE1(run_iceberg_keyagg_check_test), + CASE1(run_iceberg_pss_test), + CASE1(run_iceberg_vpss_test), + CASE1(run_iceberg_vpss_quorum_test), + CASE1(run_iceberg_keygen_test), + CASE1(run_iceberg_keygen_api_test), + CASE1(run_iceberg_dealer_kat_test), + CASE1(run_iceberg_foreign_index_test), + CASE1(run_iceberg_infinity_nonce_test), + CASE1(run_iceberg_bounds_test), + CASE1(run_iceberg_null_entry_test), + CASE1(run_iceberg_wire_test), + CASE1(run_iceberg_vectors_test), + CASE1(run_iceberg_keygen_cheat_test), + CASE1(run_iceberg_e2e_test), + CASE1(run_iceberg_c1_property_test), + CASE1(run_iceberg_parser_fuzz_test), + CASE1(run_iceberg_tweak_test), +}; + +#endif /* SECP256K1_MODULE_ICEBERG_TESTS_IMPL_H */ diff --git a/src/modules/iceberg/vectors.h b/src/modules/iceberg/vectors.h new file mode 100644 index 00000000..d15b7018 --- /dev/null +++ b/src/modules/iceberg/vectors.h @@ -0,0 +1,2446 @@ +/** + * Note: this file was autogenerated using test_vectors_iceberg_generate.py. + * Do not edit. To regenerate: + * + * git clone https://github.com/nkohen/Iceberg.git + * git -C Iceberg checkout 7b55ef6dc0dd6e11d1c14cb3bc0a675ff3487cce + * ./tools/test_vectors_iceberg_generate.py Iceberg > src/modules/iceberg/vectors.h + * + * There are no published Iceberg test vectors. These record what the reference + * does, so they catch the two implementations drifting apart, not a mistake + * they might both make. That makes this the one file here that cannot be rebuilt + * from what this repository contains, which is why the commit is named. + */ + +#include + +#define ICEBERG_VECTOR_MAX_PARTICIPANTS 9 +#define ICEBERG_VECTOR_MAX_SEEDS 126 + +struct iceberg_session_vector { + const char *label; + unsigned int n; + unsigned int t; + unsigned int mu; + size_t n_seeds; + unsigned char seeds[ICEBERG_VECTOR_MAX_SEEDS][32]; + unsigned char group_pk[33]; + unsigned char msg[32]; + unsigned char sid[32]; + unsigned char cosigner_pk[33]; + unsigned char cosigner_pubnonce[66]; + unsigned char group_aggnonce[66]; + unsigned char group_pubnonce[66]; + unsigned char group_psig[32]; + unsigned char pubshares[ICEBERG_VECTOR_MAX_PARTICIPANTS][33]; + unsigned char pubnonces[ICEBERG_VECTOR_MAX_PARTICIPANTS][66]; + unsigned char psigs[ICEBERG_VECTOR_MAX_PARTICIPANTS][32]; +}; + +static const struct iceberg_session_vector iceberg_session_vectors[] = { + { + "2of3", 3, 2, 3, 3, + { /* one seed per (t-1)-subset, in rank order */ + { + 0x9F, 0xA8, 0x99, 0xCA, 0x5B, 0xA3, 0xBE, + 0x21, 0x29, 0x70, 0x58, 0xEB, 0xD5, 0xB4, + 0xEC, 0x76, 0x48, 0x6E, 0x90, 0x25, 0x83, + 0x52, 0xC7, 0x94, 0xFE, 0x4C, 0x37, 0x89, + 0xC8, 0x9E, 0x2C, 0xEF + }, + { + 0x97, 0x81, 0xAD, 0x34, 0x80, 0xC7, 0x2B, + 0x2C, 0x90, 0x40, 0x35, 0xCA, 0xCF, 0xAD, + 0x3F, 0xDC, 0x71, 0xAF, 0x42, 0x4D, 0xC2, + 0xE7, 0x11, 0x2A, 0x8A, 0xEA, 0x91, 0x8C, + 0xBE, 0x4D, 0xED, 0x29 + }, + { + 0x10, 0xAE, 0x98, 0x22, 0x0C, 0xD5, 0xF5, + 0x7B, 0x57, 0xBF, 0x1F, 0xD9, 0x9A, 0xA2, + 0x0B, 0xDF, 0xB5, 0x11, 0x0F, 0x47, 0x2A, + 0x7B, 0x59, 0xEE, 0x60, 0xA3, 0xFF, 0x29, + 0x57, 0xE5, 0xB7, 0x59 + }, + }, + { /* group_pk */ + 0x02, 0x2E, 0x8C, 0x40, 0x09, 0x7D, 0x25, 0xA4, 0xEE, + 0x31, 0x75, 0xCF, 0x94, 0xEC, 0xED, 0x56, 0x79, 0xF3, + 0x20, 0x69, 0xBE, 0x2C, 0x6B, 0xE3, 0x00, 0x6F, 0x91, + 0x1A, 0xBD, 0x16, 0x05, 0x3B, 0x42 + }, + { /* msg */ + 0x87, 0xDF, 0x1F, 0xBE, 0xFA, 0x87, 0x42, 0x9A, 0xB7, + 0x2E, 0x71, 0xB0, 0x98, 0x6B, 0xB1, 0xC6, 0x28, 0x12, + 0x13, 0x7A, 0x5F, 0x4B, 0x65, 0xCC, 0x50, 0x82, 0x43, + 0x2D, 0x7C, 0xB8, 0xD5, 0x30 + }, + { /* sid */ + 0xFD, 0xC3, 0xA7, 0x6D, 0xA9, 0xFE, 0x39, 0x97, 0x21, + 0x9C, 0x86, 0xE1, 0x9A, 0xA2, 0x68, 0x55, 0x1E, 0xD8, + 0x3A, 0x9C, 0x97, 0x35, 0x67, 0x46, 0xDB, 0xF7, 0xA2, + 0x60, 0x7A, 0x9E, 0xFE, 0x60 + }, + { /* cosigner_pk */ + 0x03, 0x6D, 0x84, 0xFF, 0xA2, 0x3B, 0xFB, 0xE7, 0xB7, + 0xF3, 0x13, 0xCA, 0x33, 0xE9, 0x89, 0xB3, 0x30, 0x77, + 0x90, 0x95, 0x19, 0xCC, 0xE0, 0xE0, 0xDD, 0x26, 0x1F, + 0x7A, 0x72, 0xF1, 0x34, 0xAA, 0x73 + }, + { /* cosigner_pubnonce */ + 0x02, 0x08, 0xF9, 0xB7, 0x13, 0xE4, 0x94, 0x69, 0x4E, + 0x8D, 0x00, 0x62, 0x9E, 0x5C, 0xD1, 0xC6, 0x5F, 0x45, + 0xAD, 0x19, 0x75, 0x4D, 0xFC, 0x4D, 0x40, 0x72, 0x17, + 0x24, 0x58, 0x0F, 0xC0, 0xF8, 0x65, 0x03, 0x42, 0xB2, + 0x8D, 0x48, 0xAA, 0xC7, 0xD9, 0xB0, 0x93, 0x98, 0x63, + 0xB3, 0x5D, 0x7A, 0x39, 0x44, 0x6A, 0x9C, 0xA7, 0xDB, + 0x46, 0x9A, 0x86, 0xB1, 0xAC, 0xA4, 0xF9, 0x99, 0x97, + 0x05, 0x5D, 0xAA + }, + { /* group_aggnonce */ + 0x03, 0xF6, 0xD9, 0xA0, 0x14, 0xDC, 0x72, 0xEE, 0x98, + 0x45, 0xCD, 0xB2, 0x08, 0x5A, 0xD0, 0x05, 0x80, 0x7F, + 0x4D, 0xA4, 0xF6, 0xC2, 0xE2, 0x3D, 0x17, 0x75, 0xA8, + 0x9C, 0x4C, 0x2B, 0xA7, 0xED, 0xD2, 0x02, 0xA6, 0x7A, + 0xA9, 0x09, 0xE8, 0xCD, 0x2F, 0xB2, 0xA0, 0x4A, 0x28, + 0xE1, 0x15, 0x2A, 0xF8, 0x54, 0x31, 0xE6, 0xD9, 0x2E, + 0xD0, 0x41, 0x0B, 0xE7, 0xB1, 0xCE, 0x63, 0xC3, 0x2A, + 0x07, 0xD7, 0x79 + }, + { /* group_pubnonce */ + 0x03, 0xF6, 0xD9, 0xA0, 0x14, 0xDC, 0x72, 0xEE, 0x98, + 0x45, 0xCD, 0xB2, 0x08, 0x5A, 0xD0, 0x05, 0x80, 0x7F, + 0x4D, 0xA4, 0xF6, 0xC2, 0xE2, 0x3D, 0x17, 0x75, 0xA8, + 0x9C, 0x4C, 0x2B, 0xA7, 0xED, 0xD2, 0x03, 0xF2, 0x14, + 0x97, 0x2E, 0xB1, 0xB1, 0x4B, 0x36, 0x6E, 0x01, 0x0A, + 0x57, 0x28, 0x71, 0x2D, 0x77, 0x1B, 0x32, 0xCD, 0xB2, + 0x2C, 0x33, 0x0D, 0xD7, 0xC1, 0x55, 0x99, 0x63, 0x65, + 0x25, 0x0F, 0x43 + }, + { /* group_psig */ + 0x04, 0x67, 0x8E, 0x88, 0xA9, 0xD9, 0x2D, 0x71, 0xC8, + 0x58, 0x19, 0x49, 0xB3, 0x17, 0xDE, 0x43, 0x57, 0x5D, + 0x3E, 0xDF, 0x28, 0xF4, 0x61, 0x7F, 0x69, 0xBC, 0xD3, + 0x70, 0x4E, 0xC5, 0x31, 0x8D + }, + { /* pubshares, participant 1 first */ + { + 0x02, 0x62, 0x42, 0xC4, 0xDC, 0x40, 0x2D, + 0xFD, 0xD1, 0xAD, 0x86, 0xB7, 0x67, 0x03, + 0xAA, 0xFB, 0x63, 0x42, 0x4A, 0x95, 0x5F, + 0x46, 0x8D, 0x72, 0x3D, 0x25, 0x63, 0x80, + 0xBF, 0x15, 0x96, 0x9B, 0xCF + }, + { + 0x02, 0x89, 0xF4, 0x05, 0xFB, 0xF9, 0x6C, + 0xB6, 0xE1, 0x25, 0x6A, 0x1D, 0x3E, 0xA0, + 0x37, 0x83, 0x4D, 0xE9, 0x2E, 0xFF, 0xD3, + 0x9A, 0x45, 0x20, 0x04, 0xCB, 0xD6, 0x81, + 0x9C, 0xFD, 0x2D, 0x01, 0x40 + }, + { + 0x02, 0x0E, 0xD1, 0x97, 0xB4, 0x79, 0xC2, + 0x43, 0x6D, 0x7A, 0x4E, 0x92, 0x76, 0x0E, + 0x97, 0x85, 0x17, 0x6D, 0x1C, 0x8A, 0x49, + 0x8A, 0x21, 0x8A, 0x6D, 0xA9, 0x91, 0xD4, + 0xDB, 0x53, 0x1B, 0x03, 0x04 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x03, 0xFB, 0x5B, 0xC0, 0x01, 0x98, 0x4E, + 0x1E, 0x2B, 0x9F, 0xF2, 0xDD, 0x9B, 0x65, + 0x88, 0xBD, 0x66, 0x9F, 0x53, 0x52, 0xE3, + 0x7E, 0x36, 0xA6, 0xF8, 0xD8, 0x4E, 0x97, + 0xEF, 0x55, 0xE4, 0x6C, 0xC2, 0x03, 0xFC, + 0x4D, 0x99, 0x02, 0x88, 0x09, 0x67, 0xD5, + 0xBC, 0xD4, 0x41, 0x9D, 0x67, 0x1C, 0x9C, + 0x64, 0xC6, 0x1D, 0x60, 0xA1, 0xA2, 0xCE, + 0xE8, 0xEF, 0x67, 0x39, 0xE0, 0x85, 0x56, + 0xD5, 0x98, 0xEC + }, + { + 0x03, 0x5E, 0x2C, 0x92, 0x7D, 0x5B, 0xF4, + 0xF5, 0x0D, 0xC6, 0xA0, 0x3C, 0x99, 0xD4, + 0x11, 0x8A, 0xB2, 0xDF, 0x85, 0xB2, 0x8C, + 0x45, 0xE1, 0x57, 0xEB, 0x3C, 0x67, 0x53, + 0xD0, 0xF9, 0x58, 0x7F, 0x60, 0x02, 0x2C, + 0x70, 0x09, 0xB9, 0x13, 0x4D, 0x90, 0x6E, + 0xC3, 0xDA, 0x51, 0x95, 0x1F, 0x0B, 0x9E, + 0x81, 0x08, 0x68, 0x46, 0x05, 0x7C, 0x24, + 0x06, 0x35, 0x8A, 0x82, 0xA9, 0xDA, 0x9B, + 0x0B, 0x2A, 0x00 + }, + { + 0x02, 0x77, 0x35, 0x2C, 0x49, 0xA7, 0x65, + 0x46, 0xE6, 0x0D, 0x01, 0xD6, 0xE6, 0x34, + 0x23, 0x3A, 0x9A, 0x15, 0x8E, 0xD1, 0x18, + 0x3E, 0x89, 0x6B, 0x7D, 0xEB, 0xAC, 0x09, + 0xD9, 0xDE, 0x56, 0xBB, 0x64, 0x02, 0xE4, + 0x9E, 0x37, 0xDC, 0xEA, 0x4D, 0xD4, 0x80, + 0x20, 0x3A, 0xB8, 0xFD, 0xD5, 0x54, 0x9F, + 0x3E, 0x11, 0x8A, 0x1C, 0x68, 0xCD, 0xC5, + 0x76, 0xE8, 0x79, 0xA8, 0x81, 0xAC, 0x1C, + 0x46, 0xB7, 0x08 + }, + }, + { /* signature shares, the first t participants */ + { + 0xCD, 0x51, 0xC2, 0x9A, 0xDA, 0x7C, 0x1A, + 0x2D, 0xDA, 0xF3, 0xD5, 0x84, 0x5F, 0x80, + 0xFD, 0xAE, 0x03, 0x94, 0x1E, 0x8C, 0x33, + 0x8E, 0x68, 0x10, 0x50, 0x81, 0xE9, 0x7E, + 0x27, 0xBF, 0x7C, 0x3E + }, + { + 0x96, 0x3B, 0xF6, 0xAD, 0x0B, 0x1F, 0x06, + 0xE9, 0xED, 0x8F, 0x91, 0xBF, 0x0B, 0xEA, + 0x1D, 0x19, 0xF5, 0x1C, 0x21, 0x52, 0x8E, + 0xDF, 0xCE, 0x65, 0x77, 0x74, 0xA0, 0xFF, + 0x30, 0x83, 0x85, 0xAE + }, + }, + }, + { + "3of5", 5, 3, 5, 10, + { /* one seed per (t-1)-subset, in rank order */ + { + 0xC8, 0x4C, 0x03, 0x72, 0x7B, 0xD2, 0x1B, + 0x38, 0xD6, 0x0D, 0x8D, 0xB7, 0xC8, 0x36, + 0x15, 0x15, 0x21, 0xDC, 0x0D, 0x98, 0x93, + 0x8A, 0xB4, 0xA4, 0x78, 0x1F, 0xAE, 0xC1, + 0x25, 0xA8, 0x97, 0xF3 + }, + { + 0xCE, 0x91, 0x69, 0xDD, 0x9C, 0x35, 0x4D, + 0x62, 0x39, 0xA8, 0x8C, 0xC0, 0xC1, 0xEA, + 0x8A, 0x0B, 0x58, 0xB0, 0x77, 0x55, 0x4E, + 0x5C, 0x63, 0xB5, 0x19, 0xAE, 0x40, 0x31, + 0x81, 0x98, 0x8B, 0x2D + }, + { + 0x28, 0xAB, 0xC4, 0xC2, 0x24, 0x06, 0xCA, + 0x3B, 0x8F, 0xC9, 0x37, 0x25, 0x31, 0xC6, + 0x30, 0xF3, 0x78, 0x03, 0x7C, 0xDF, 0x6F, + 0x75, 0xA3, 0x1E, 0x44, 0x46, 0x34, 0xD2, + 0xC0, 0xA5, 0xEB, 0x8E + }, + { + 0xAC, 0x33, 0x26, 0x3A, 0x42, 0x77, 0x03, + 0x2E, 0x85, 0xB6, 0xEC, 0xD5, 0x1A, 0xCE, + 0xAA, 0x02, 0x31, 0xD8, 0x4E, 0x99, 0x4F, + 0x6A, 0xB6, 0x8C, 0x30, 0x82, 0x0A, 0x3B, + 0xB9, 0x2C, 0xA7, 0x48 + }, + { + 0xC6, 0x70, 0xA6, 0x6E, 0x79, 0xC5, 0x80, + 0x8F, 0x7A, 0xEB, 0xE6, 0xC1, 0x1D, 0x1C, + 0xB6, 0x61, 0x00, 0x5B, 0xB5, 0x75, 0x7F, + 0x73, 0x7C, 0xA9, 0x39, 0xDA, 0x8A, 0x68, + 0x84, 0x21, 0x92, 0xBD + }, + { + 0xA8, 0x69, 0x98, 0xDE, 0x0B, 0xD1, 0x7C, + 0x27, 0x34, 0x25, 0xCE, 0xF0, 0xBF, 0xFB, + 0x37, 0x94, 0xAC, 0xDB, 0x67, 0x2B, 0xD7, + 0xBA, 0x77, 0x26, 0x22, 0xF7, 0x32, 0x0C, + 0x37, 0x88, 0x0A, 0x4A + }, + { + 0x67, 0x58, 0x8C, 0xD3, 0xBA, 0x94, 0xF5, + 0x78, 0xBB, 0x04, 0x6D, 0xE0, 0x00, 0xDA, + 0x47, 0x23, 0xF5, 0xC1, 0xD0, 0xB0, 0xCE, + 0x70, 0xA3, 0x50, 0x32, 0x60, 0x36, 0x15, + 0x5A, 0x84, 0x83, 0x59 + }, + { + 0xD1, 0x4F, 0x88, 0xAE, 0x99, 0xDC, 0x70, + 0xBB, 0xC8, 0xC9, 0x3C, 0x5B, 0xAB, 0xF4, + 0x39, 0x6A, 0x59, 0xCF, 0xF0, 0x2F, 0x5A, + 0x60, 0x1B, 0x98, 0x42, 0x32, 0x4D, 0x02, + 0xD4, 0x1E, 0xF3, 0xF5 + }, + { + 0x8C, 0x3F, 0xE7, 0x2F, 0x98, 0x9F, 0x23, + 0xC5, 0x09, 0xC2, 0x72, 0x91, 0x5F, 0xD0, + 0x15, 0x84, 0x8A, 0x9E, 0x82, 0xA2, 0xB9, + 0xE2, 0xE4, 0x27, 0x56, 0x7F, 0x3E, 0x37, + 0x50, 0x19, 0xA5, 0x5E + }, + { + 0x42, 0xAE, 0x91, 0x42, 0x17, 0x2B, 0x39, + 0xE5, 0xEF, 0xBE, 0xEE, 0xCA, 0x0B, 0x1D, + 0xAF, 0x05, 0xA1, 0x17, 0x95, 0xC4, 0xDE, + 0xBF, 0x18, 0x19, 0x8D, 0x53, 0x92, 0x4C, + 0x10, 0xA3, 0xC6, 0x80 + }, + }, + { /* group_pk */ + 0x03, 0xF7, 0x5D, 0x9C, 0xE0, 0xFF, 0x0E, 0xBA, 0xC8, + 0x81, 0x4C, 0x2B, 0x9F, 0x15, 0xD4, 0x9C, 0x83, 0x50, + 0x3B, 0x5D, 0x26, 0xEB, 0x81, 0x9C, 0xC1, 0x90, 0xC0, + 0x44, 0x2B, 0x50, 0x39, 0xD2, 0x8B + }, + { /* msg */ + 0x3A, 0x08, 0xAE, 0x76, 0x9F, 0x7F, 0x64, 0x6B, 0xFC, + 0x54, 0x7E, 0x8F, 0x39, 0x77, 0x63, 0x2C, 0x64, 0x44, + 0x9A, 0xD7, 0x5E, 0x5D, 0x9C, 0xDD, 0xC4, 0xDC, 0x22, + 0x0A, 0x6F, 0xCC, 0x3C, 0x31 + }, + { /* sid */ + 0xF8, 0x94, 0xCE, 0x56, 0x9A, 0x98, 0xEC, 0x6A, 0x97, + 0x1B, 0x48, 0x30, 0x60, 0x56, 0x16, 0x13, 0xD1, 0x21, + 0x72, 0xC1, 0x2B, 0xE1, 0x09, 0xB5, 0xBF, 0xE7, 0xB3, + 0x26, 0xB8, 0x21, 0x1F, 0x5C + }, + { /* cosigner_pk */ + 0x02, 0x0C, 0x2B, 0xE9, 0x93, 0xC9, 0xED, 0x08, 0x86, + 0xFD, 0xF6, 0x92, 0xFA, 0x87, 0x3E, 0xC5, 0xB2, 0x88, + 0xFD, 0x75, 0x7C, 0x2F, 0x06, 0xE9, 0xBE, 0xA6, 0xE6, + 0xA1, 0xFC, 0x87, 0xC8, 0xE5, 0xB3 + }, + { /* cosigner_pubnonce */ + 0x03, 0x3A, 0x69, 0xDF, 0xCB, 0xB8, 0x35, 0x7C, 0xED, + 0xFD, 0x24, 0xBD, 0x59, 0xBC, 0x9C, 0xA8, 0x60, 0x7E, + 0x5C, 0xC3, 0x59, 0xF4, 0x42, 0xF0, 0xD2, 0xA5, 0xE4, + 0x85, 0xA0, 0x79, 0xFC, 0xD5, 0x12, 0x02, 0x59, 0x5C, + 0x62, 0xF1, 0x6E, 0x2E, 0xDD, 0x34, 0x96, 0x77, 0x54, + 0x01, 0xC7, 0xA9, 0x70, 0xCA, 0xD5, 0x04, 0x75, 0x64, + 0x9B, 0x81, 0x54, 0x92, 0x0B, 0xC8, 0x0D, 0x73, 0x6A, + 0x34, 0xF0, 0xC6 + }, + { /* group_aggnonce */ + 0x02, 0x4D, 0x52, 0x7D, 0xF9, 0x48, 0x79, 0x38, 0x2C, + 0xFB, 0x92, 0xE2, 0x8A, 0x18, 0x4C, 0x5A, 0x92, 0xB1, + 0xC7, 0x70, 0x41, 0xDC, 0x16, 0xDD, 0x18, 0x48, 0xFC, + 0x42, 0x53, 0x66, 0x63, 0x0B, 0x38, 0x02, 0xC8, 0xE6, + 0x34, 0x1E, 0x30, 0x96, 0xCB, 0xDF, 0x5C, 0xE3, 0xF6, + 0x4B, 0xE8, 0x44, 0x27, 0xE4, 0x38, 0xAD, 0xDB, 0xA7, + 0xD0, 0xDC, 0x29, 0xA7, 0xDC, 0x21, 0xF4, 0xAD, 0x42, + 0x65, 0x12, 0xFF + }, + { /* group_pubnonce */ + 0x02, 0x4D, 0x52, 0x7D, 0xF9, 0x48, 0x79, 0x38, 0x2C, + 0xFB, 0x92, 0xE2, 0x8A, 0x18, 0x4C, 0x5A, 0x92, 0xB1, + 0xC7, 0x70, 0x41, 0xDC, 0x16, 0xDD, 0x18, 0x48, 0xFC, + 0x42, 0x53, 0x66, 0x63, 0x0B, 0x38, 0x03, 0x56, 0x46, + 0xE9, 0xBD, 0xCF, 0xC0, 0xD9, 0xE6, 0x42, 0x9A, 0xD2, + 0x12, 0x48, 0x6C, 0x16, 0xBE, 0x56, 0x8D, 0x9C, 0xD7, + 0xC1, 0x0D, 0xB3, 0xED, 0x1D, 0xB0, 0x91, 0xAA, 0x23, + 0x1B, 0x20, 0x15 + }, + { /* group_psig */ + 0x43, 0xD5, 0xBB, 0x3F, 0x15, 0xFA, 0xAD, 0x42, 0x7D, + 0x3A, 0x5B, 0x66, 0xF2, 0xC9, 0x9A, 0xC4, 0xB8, 0xB2, + 0x25, 0x1A, 0x46, 0xFF, 0x15, 0x9E, 0x77, 0xF8, 0x37, + 0xA9, 0xDC, 0x33, 0xA9, 0x00 + }, + { /* pubshares, participant 1 first */ + { + 0x03, 0x7C, 0x8D, 0x04, 0x3A, 0x28, 0x53, + 0x1B, 0x44, 0xD9, 0xD7, 0xD6, 0xDC, 0xC6, + 0xF3, 0x43, 0x95, 0xC9, 0x1D, 0xA7, 0x1D, + 0x53, 0x9F, 0xD7, 0xE1, 0xE4, 0xEE, 0xB0, + 0x80, 0x2F, 0xF6, 0x35, 0x71 + }, + { + 0x02, 0x2E, 0xFA, 0x79, 0x67, 0xDA, 0xA1, + 0x67, 0xCB, 0x31, 0xDC, 0x4A, 0x33, 0x65, + 0x4C, 0x3C, 0x13, 0x6D, 0xD5, 0x71, 0x9B, + 0x83, 0xEC, 0xF6, 0x3D, 0x08, 0x11, 0x47, + 0xEC, 0x8C, 0xC1, 0xD0, 0x0A + }, + { + 0x03, 0x0E, 0xE0, 0x9A, 0xA0, 0xAB, 0x0A, + 0xCF, 0x04, 0x63, 0x99, 0xB5, 0xE7, 0xDA, + 0xD8, 0x2D, 0x03, 0xA1, 0x28, 0xB7, 0x31, + 0xBB, 0x15, 0x78, 0xF0, 0x43, 0x50, 0xED, + 0x84, 0x83, 0xDE, 0x4D, 0x61 + }, + { + 0x03, 0xF3, 0x99, 0xEF, 0x5B, 0x42, 0xF5, + 0xF9, 0x1B, 0xDB, 0x55, 0xBD, 0x28, 0xBD, + 0x2E, 0xCB, 0xC6, 0xC2, 0x4A, 0xEA, 0xB4, + 0x6C, 0x25, 0x02, 0x5F, 0xE1, 0x90, 0xDF, + 0xE7, 0x4C, 0x76, 0x9A, 0xF6 + }, + { + 0x02, 0x8A, 0xC9, 0x95, 0x39, 0x15, 0x0F, + 0xE7, 0x45, 0xAB, 0x05, 0x21, 0xAB, 0x99, + 0x6D, 0x6D, 0x72, 0xFD, 0x3D, 0xCA, 0x11, + 0x9B, 0x19, 0xFB, 0x57, 0xD3, 0xA0, 0xE4, + 0xCD, 0x4B, 0x28, 0x1B, 0xBD + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x02, 0xFF, 0xF3, 0x59, 0x35, 0xE0, 0x0B, + 0x3F, 0xAB, 0xD9, 0xF9, 0x98, 0x92, 0x5C, + 0x90, 0xEB, 0xCC, 0x9E, 0x4F, 0x7F, 0xA8, + 0x1C, 0xA3, 0x18, 0x3A, 0x5D, 0xAC, 0x45, + 0x24, 0x06, 0x3E, 0x76, 0x2A, 0x02, 0x70, + 0xE4, 0x98, 0x52, 0x7B, 0xF7, 0x1A, 0xCF, + 0x2C, 0x16, 0xC8, 0x5C, 0x82, 0x5D, 0x51, + 0xE2, 0xBA, 0x14, 0x4C, 0xA4, 0xAD, 0xCB, + 0xAC, 0xC1, 0x6B, 0xF8, 0x33, 0x47, 0x9C, + 0xCE, 0x75, 0xD1 + }, + { + 0x03, 0x20, 0x0E, 0x62, 0x8B, 0x01, 0x4E, + 0xD4, 0xEA, 0x5C, 0xBA, 0xA1, 0x9C, 0xDF, + 0x76, 0x40, 0x06, 0x89, 0x61, 0x50, 0x3E, + 0xC3, 0xEE, 0x5E, 0x4D, 0xE8, 0xFA, 0x8D, + 0x51, 0xEC, 0xD7, 0xB4, 0xAF, 0x03, 0xFA, + 0xD6, 0xDB, 0x0F, 0x05, 0x9A, 0x0D, 0xA1, + 0x43, 0x12, 0xC9, 0x4A, 0xD3, 0x9C, 0x9D, + 0xF9, 0xCD, 0x3C, 0x03, 0x50, 0x50, 0x0F, + 0xEC, 0xFC, 0x9E, 0x2F, 0x69, 0x6C, 0xC0, + 0xC3, 0xB3, 0x51 + }, + { + 0x03, 0x8A, 0x12, 0x1B, 0x78, 0xC7, 0x60, + 0x54, 0xC2, 0xFF, 0xE4, 0xF1, 0xB9, 0x1F, + 0xE8, 0x97, 0xE6, 0xE7, 0x78, 0x4A, 0xEB, + 0xBB, 0x11, 0x15, 0x92, 0xF5, 0x68, 0x80, + 0x7A, 0x05, 0x1C, 0x84, 0xF0, 0x02, 0x02, + 0x10, 0x9F, 0xCA, 0xCE, 0x30, 0x20, 0x3D, + 0x68, 0x98, 0x05, 0xDD, 0xE9, 0xCB, 0x66, + 0xE7, 0x9F, 0x53, 0x7F, 0x5E, 0xA5, 0x27, + 0x9D, 0x72, 0x99, 0xEB, 0x8D, 0xAB, 0xAB, + 0xF3, 0x48, 0x9A + }, + { + 0x03, 0x17, 0x02, 0xE2, 0x05, 0x1B, 0x35, + 0x40, 0x41, 0x46, 0x2C, 0x9C, 0x27, 0x9C, + 0x25, 0xC4, 0xEA, 0x90, 0x1E, 0xB3, 0x85, + 0x00, 0x9D, 0x9A, 0x6D, 0x34, 0x36, 0x72, + 0x3B, 0x4D, 0x65, 0x6E, 0xF1, 0x02, 0xA7, + 0xD9, 0xC7, 0x57, 0x9C, 0xEA, 0xB0, 0x80, + 0x6D, 0x6B, 0x18, 0x1D, 0x7D, 0x9E, 0x2D, + 0xF2, 0xCA, 0x67, 0x62, 0x8E, 0xAF, 0x15, + 0x7F, 0x7F, 0x80, 0x45, 0x62, 0x76, 0xB8, + 0xC5, 0x96, 0x51 + }, + { + 0x03, 0xEB, 0xBC, 0xB0, 0x88, 0x1C, 0xF2, + 0xD8, 0x36, 0x64, 0xE7, 0xBB, 0xA1, 0x36, + 0xC0, 0xA4, 0x22, 0x79, 0x8E, 0xEC, 0x9C, + 0x40, 0x84, 0xBC, 0x23, 0x98, 0xBD, 0xB4, + 0xE6, 0xEC, 0x03, 0xB6, 0x6D, 0x02, 0x06, + 0x5C, 0x57, 0xD0, 0x4C, 0x0A, 0xFF, 0x9C, + 0x51, 0xA3, 0xC4, 0x60, 0xFB, 0xAE, 0x36, + 0x00, 0x88, 0xB4, 0xB9, 0xE2, 0x14, 0x0F, + 0x24, 0xFF, 0xF7, 0x0D, 0x6B, 0x38, 0xA0, + 0x22, 0x5D, 0xAB + }, + }, + { /* signature shares, the first t participants */ + { + 0x29, 0x1D, 0x1E, 0xE1, 0x6A, 0xD7, 0xBB, + 0x42, 0x88, 0xB0, 0xB3, 0x8B, 0x5C, 0x62, + 0xEB, 0xD3, 0xCC, 0x27, 0xC8, 0xF8, 0x29, + 0xE2, 0x92, 0x2F, 0x7C, 0x47, 0xFC, 0x7F, + 0x1A, 0xB2, 0x5F, 0xE6 + }, + { + 0x91, 0xD7, 0x06, 0x68, 0xA6, 0xEF, 0x80, + 0xF8, 0x33, 0x7D, 0x28, 0x33, 0xDB, 0x1B, + 0xDA, 0x18, 0xD2, 0x91, 0xEC, 0x2C, 0x71, + 0x85, 0x08, 0x9A, 0x61, 0x3D, 0x58, 0x01, + 0x5F, 0x9C, 0x6B, 0xD7 + }, + { + 0x7E, 0x03, 0x71, 0xD4, 0xCA, 0x41, 0xFE, + 0x63, 0x7D, 0x9F, 0xB9, 0x60, 0x6E, 0xF4, + 0x65, 0x95, 0x11, 0x41, 0xB1, 0xD0, 0x6E, + 0x9D, 0xD8, 0xA3, 0x67, 0x05, 0xEB, 0xA3, + 0xDA, 0xBB, 0x8B, 0x92 + }, + }, + }, + { + "3of7", 7, 3, 5, 21, + { /* one seed per (t-1)-subset, in rank order */ + { + 0xE1, 0xA7, 0x4E, 0xA3, 0xE2, 0x9B, 0x48, + 0x39, 0x3C, 0x29, 0x28, 0x77, 0x44, 0x5B, + 0xBA, 0x31, 0xF1, 0x70, 0x9B, 0x9B, 0x2C, + 0xE5, 0x2B, 0x5D, 0xFA, 0x6F, 0x76, 0x62, + 0xF0, 0xE0, 0x66, 0x50 + }, + { + 0xEF, 0x62, 0x00, 0x9D, 0x07, 0x22, 0xE6, + 0x4F, 0xE8, 0x05, 0x39, 0x9B, 0x73, 0x82, + 0x66, 0xD3, 0x17, 0x9A, 0xB4, 0x48, 0x34, + 0xB2, 0xA9, 0x49, 0xCE, 0x26, 0xB6, 0x7D, + 0x9E, 0x60, 0xF6, 0xCF + }, + { + 0x18, 0x1B, 0xDD, 0x98, 0x1E, 0x06, 0x85, + 0xDA, 0x76, 0xC3, 0xC0, 0x50, 0xEA, 0x4C, + 0x71, 0x6A, 0xAF, 0xB4, 0x84, 0x9A, 0xF1, + 0x1D, 0xD3, 0xD1, 0x00, 0x02, 0xCE, 0x19, + 0xC4, 0x4D, 0xAF, 0x27 + }, + { + 0xB7, 0x48, 0x20, 0x00, 0x8A, 0x59, 0xCA, + 0x45, 0x2F, 0x0C, 0x2B, 0x1B, 0xC8, 0x9D, + 0xF8, 0x64, 0xBB, 0x40, 0x57, 0x40, 0xA2, + 0xA5, 0xCE, 0x6B, 0x83, 0x16, 0x22, 0xD7, + 0x86, 0xF0, 0xB4, 0x3F + }, + { + 0x0D, 0xEE, 0x4C, 0x98, 0x07, 0xF3, 0xE7, + 0xB4, 0x2F, 0x74, 0x21, 0xF8, 0xCB, 0xEE, + 0x9D, 0xEB, 0x7F, 0xD5, 0x2E, 0x31, 0xCE, + 0xAB, 0xB9, 0x3E, 0x5F, 0x52, 0xA9, 0xF3, + 0x7F, 0xE5, 0x76, 0xC9 + }, + { + 0x16, 0x67, 0x5D, 0xED, 0x27, 0x5B, 0xEE, + 0x94, 0x44, 0x3B, 0x57, 0xC3, 0x32, 0xA4, + 0x1C, 0x17, 0x40, 0x1A, 0x20, 0xD6, 0xDD, + 0xA8, 0x69, 0x25, 0xE5, 0x0E, 0x96, 0x0A, + 0x36, 0x93, 0xF4, 0xBF + }, + { + 0x58, 0x65, 0x59, 0x81, 0x2B, 0x8A, 0xDF, + 0x52, 0xE4, 0xFB, 0x89, 0x95, 0x69, 0x31, + 0xDC, 0x21, 0xE8, 0x8E, 0xD5, 0xD6, 0x48, + 0x3D, 0xA9, 0xB9, 0xB4, 0x1F, 0x6B, 0x70, + 0x8A, 0xFE, 0x7B, 0xE8 + }, + { + 0x92, 0x9A, 0x0F, 0x46, 0xA3, 0x0E, 0x9F, + 0xB7, 0x3E, 0xED, 0x3B, 0x26, 0xB9, 0x49, + 0x9F, 0x29, 0x95, 0x82, 0x70, 0x21, 0x71, + 0x3B, 0x31, 0x2F, 0x49, 0xB2, 0xFC, 0x86, + 0xAE, 0xB8, 0x18, 0x23 + }, + { + 0x71, 0x72, 0x00, 0xA8, 0x26, 0x77, 0xBC, + 0x97, 0xD5, 0x50, 0x79, 0x44, 0xB8, 0x1D, + 0x60, 0x2E, 0x05, 0xAA, 0x02, 0x47, 0xA1, + 0x81, 0xA5, 0xED, 0x7F, 0x96, 0x57, 0x15, + 0xA3, 0xAD, 0xD5, 0x5D + }, + { + 0xEE, 0x9A, 0x95, 0xB5, 0xF3, 0xAD, 0x2C, + 0x59, 0x72, 0x27, 0x17, 0x90, 0x81, 0x4E, + 0x65, 0x9A, 0xEF, 0x5E, 0x22, 0x2C, 0x2C, + 0x19, 0x66, 0x76, 0x13, 0x7D, 0x52, 0x86, + 0xF6, 0xC5, 0xFC, 0xB3 + }, + { + 0xD8, 0x9B, 0x46, 0x9C, 0x07, 0xE1, 0xBB, + 0xDC, 0x0C, 0x0F, 0x46, 0xE3, 0x4F, 0x73, + 0x94, 0xA8, 0xBA, 0x0B, 0xD2, 0xC7, 0x55, + 0xCA, 0x13, 0xED, 0x0A, 0x60, 0x3A, 0xA9, + 0xFA, 0xAC, 0x66, 0xF4 + }, + { + 0x90, 0x36, 0x57, 0xCE, 0x6D, 0x80, 0xB0, + 0x05, 0x17, 0xE6, 0x5F, 0xF4, 0x48, 0x1C, + 0xE4, 0x39, 0xA2, 0xD4, 0x6A, 0xFA, 0x1E, + 0xC6, 0xEB, 0xC8, 0x5A, 0x10, 0x6B, 0xB3, + 0x7B, 0x74, 0x8D, 0x44 + }, + { + 0xC8, 0x2F, 0x25, 0x58, 0xD6, 0x11, 0x23, + 0x15, 0x9E, 0xCA, 0xAD, 0xF1, 0xB1, 0x4E, + 0x55, 0x85, 0x8D, 0xC5, 0xBC, 0xDD, 0x3D, + 0x44, 0x61, 0xE9, 0x11, 0x5C, 0x5D, 0x30, + 0xFF, 0xE2, 0x46, 0xCE + }, + { + 0x80, 0xE0, 0xA4, 0xD0, 0xCB, 0x99, 0xE0, + 0xEB, 0x85, 0x0F, 0x67, 0xAE, 0x10, 0xB9, + 0x60, 0xAF, 0x64, 0x6E, 0x8A, 0xEC, 0xD9, + 0x8C, 0x10, 0x24, 0xB5, 0xF6, 0x46, 0x85, + 0x3F, 0x60, 0xA6, 0xEE + }, + { + 0x1E, 0xDB, 0xE5, 0x29, 0xCD, 0xAD, 0xFA, + 0x33, 0x96, 0x0A, 0x09, 0xF0, 0x8D, 0x0F, + 0x00, 0x61, 0xFB, 0x7E, 0xCD, 0x9C, 0xAD, + 0xE5, 0xF6, 0x4B, 0x29, 0x40, 0xA8, 0x00, + 0xE0, 0x07, 0xEB, 0xC1 + }, + { + 0x5D, 0x2C, 0xEC, 0x20, 0xDC, 0x6A, 0xE4, + 0x57, 0x5D, 0x4D, 0xCE, 0x0F, 0x77, 0x5C, + 0xBD, 0x0D, 0x2D, 0xF2, 0x05, 0x48, 0x93, + 0x33, 0x07, 0x52, 0xEB, 0xB7, 0x18, 0xC2, + 0x71, 0xDA, 0x4C, 0x6C + }, + { + 0xA7, 0xAE, 0xFB, 0xCE, 0x4A, 0xDA, 0x43, + 0x86, 0x39, 0x7F, 0x42, 0x79, 0xDF, 0xAC, + 0xD9, 0xEA, 0x9B, 0xFB, 0x7B, 0xE1, 0x06, + 0x3B, 0x20, 0xA7, 0x58, 0xF0, 0x46, 0x8F, + 0x11, 0xCD, 0xEF, 0x84 + }, + { + 0xC3, 0xFA, 0x8B, 0x8B, 0x0B, 0x1B, 0x7B, + 0xC5, 0xE0, 0xA7, 0xE4, 0x6F, 0x16, 0x1C, + 0xBD, 0x22, 0xF9, 0xA5, 0x95, 0x35, 0xF4, + 0xF1, 0xCF, 0xD8, 0xC0, 0x5D, 0xEB, 0x84, + 0xB1, 0xAA, 0x18, 0x2B + }, + { + 0x0A, 0xDA, 0x69, 0xFB, 0x82, 0x99, 0xBE, + 0x87, 0xB5, 0x9F, 0x8D, 0x7A, 0x55, 0x5D, + 0x88, 0xDF, 0x7E, 0x78, 0x5A, 0xD0, 0xC0, + 0x01, 0x27, 0x0E, 0x9F, 0xCB, 0xD0, 0x60, + 0x66, 0x50, 0x09, 0x71 + }, + { + 0x04, 0xF9, 0x27, 0x05, 0x18, 0xA0, 0x4F, + 0x7C, 0x5A, 0xF9, 0xEA, 0xD5, 0xC0, 0x75, + 0xC5, 0x05, 0xD7, 0xF0, 0x5D, 0x12, 0x72, + 0x3C, 0x79, 0x69, 0xCE, 0xF8, 0x90, 0x3F, + 0x71, 0x2D, 0xDE, 0xC0 + }, + { + 0xBB, 0x19, 0x98, 0xDD, 0xEA, 0xCB, 0xB2, + 0xF9, 0x00, 0x91, 0x84, 0x37, 0x81, 0x69, + 0x47, 0xFE, 0x07, 0xFD, 0x3C, 0xC1, 0x32, + 0x8E, 0x5E, 0xCF, 0x27, 0x2E, 0xA1, 0x4F, + 0xA6, 0x0E, 0xF4, 0xEA + }, + }, + { /* group_pk */ + 0x03, 0x20, 0x2C, 0x04, 0x57, 0xC7, 0xEB, 0x28, 0xEC, + 0x99, 0xAB, 0xEE, 0x04, 0xCF, 0x12, 0x55, 0x45, 0xC2, + 0x65, 0xF7, 0x5C, 0x26, 0x40, 0x58, 0x88, 0x99, 0x26, + 0x31, 0x61, 0xC2, 0x07, 0xD6, 0x25 + }, + { /* msg */ + 0xE6, 0xD2, 0x45, 0x9D, 0x0E, 0x22, 0xD9, 0x49, 0xFB, + 0xAF, 0x5D, 0x41, 0xC6, 0x4D, 0x22, 0x7D, 0x97, 0xAF, + 0x2F, 0x53, 0x52, 0x23, 0x7E, 0x5E, 0x07, 0x73, 0x1D, + 0x0A, 0x88, 0xA8, 0x68, 0x4A + }, + { /* sid */ + 0x73, 0x79, 0x75, 0x45, 0x65, 0x02, 0xC3, 0x34, 0x88, + 0x27, 0xFB, 0x8D, 0x2A, 0x75, 0x62, 0xA3, 0x37, 0x38, + 0x87, 0xD3, 0x24, 0x3B, 0xBC, 0x92, 0xFB, 0xF7, 0x12, + 0x71, 0x37, 0x4C, 0x63, 0x98 + }, + { /* cosigner_pk */ + 0x03, 0x92, 0x17, 0xED, 0x6B, 0x3D, 0x0A, 0xF6, 0x3E, + 0xB1, 0x2A, 0x80, 0x16, 0x87, 0x7B, 0x98, 0x2B, 0x5B, + 0xBF, 0x67, 0x16, 0xFE, 0x2B, 0x8A, 0x5C, 0x6D, 0x53, + 0x8B, 0xC1, 0x88, 0x7C, 0x20, 0x6D + }, + { /* cosigner_pubnonce */ + 0x02, 0x6B, 0x48, 0x73, 0x75, 0x85, 0x2A, 0x22, 0x98, + 0x79, 0x8C, 0x4F, 0x5A, 0xF8, 0x03, 0x4C, 0x20, 0xAC, + 0xD8, 0x46, 0x11, 0x82, 0x54, 0x10, 0xCF, 0xA2, 0x70, + 0xC5, 0xA3, 0x91, 0x08, 0x5E, 0x7C, 0x03, 0xB1, 0x84, + 0xEA, 0xB5, 0xC1, 0xE2, 0x30, 0xCB, 0x52, 0x87, 0x07, + 0x85, 0x26, 0xE9, 0xFE, 0xD1, 0xE9, 0xD3, 0x8A, 0x49, + 0x99, 0x24, 0x24, 0x92, 0x03, 0x12, 0x42, 0x8E, 0x1B, + 0xCD, 0x0E, 0xF9 + }, + { /* group_aggnonce */ + 0x02, 0x5A, 0xE2, 0xED, 0x71, 0x62, 0x4B, 0x63, 0x5D, + 0x95, 0x78, 0xA2, 0x24, 0x18, 0x0E, 0xBA, 0x7B, 0x29, + 0x8E, 0x3A, 0x13, 0xEA, 0xF3, 0xE2, 0xB1, 0x51, 0x96, + 0xE0, 0x69, 0x14, 0xF1, 0xD9, 0xA9, 0x03, 0x97, 0x01, + 0xD7, 0x7E, 0x78, 0xCD, 0x6E, 0x70, 0x75, 0x93, 0x12, + 0xBE, 0x82, 0xD5, 0x81, 0x93, 0x2F, 0x54, 0xDB, 0x13, + 0xF0, 0x58, 0xC5, 0xF9, 0xC4, 0x9A, 0x73, 0xBA, 0x99, + 0xF1, 0x23, 0x0A + }, + { /* group_pubnonce */ + 0x02, 0x5A, 0xE2, 0xED, 0x71, 0x62, 0x4B, 0x63, 0x5D, + 0x95, 0x78, 0xA2, 0x24, 0x18, 0x0E, 0xBA, 0x7B, 0x29, + 0x8E, 0x3A, 0x13, 0xEA, 0xF3, 0xE2, 0xB1, 0x51, 0x96, + 0xE0, 0x69, 0x14, 0xF1, 0xD9, 0xA9, 0x02, 0xFD, 0x20, + 0xD5, 0x5B, 0x84, 0x0C, 0x24, 0xF9, 0x79, 0x6E, 0xB6, + 0x39, 0xFE, 0xF4, 0x2A, 0x6F, 0x9C, 0xA0, 0x0C, 0x8C, + 0x49, 0xCE, 0x90, 0x10, 0x52, 0x85, 0xE6, 0xA5, 0x38, + 0x9B, 0xE1, 0xB3 + }, + { /* group_psig */ + 0xEB, 0xBC, 0x9F, 0x00, 0x93, 0xE2, 0xBF, 0x6C, 0x72, + 0xB3, 0x3D, 0xCD, 0xB7, 0x4E, 0x0C, 0x27, 0x20, 0x22, + 0xF2, 0x72, 0x79, 0xF2, 0xC6, 0xED, 0x19, 0xF9, 0xD3, + 0x5F, 0xFE, 0xF7, 0xC5, 0xDB + }, + { /* pubshares, participant 1 first */ + { + 0x03, 0x68, 0xC2, 0x21, 0x98, 0xC3, 0x52, + 0x62, 0xF8, 0x71, 0x80, 0x57, 0xAF, 0x52, + 0x12, 0xC0, 0x14, 0xCB, 0x23, 0x09, 0x0C, + 0x7F, 0x9C, 0x08, 0xAB, 0xF8, 0xF4, 0x9F, + 0x36, 0xD3, 0x14, 0x4D, 0x53 + }, + { + 0x02, 0xBD, 0x1B, 0x71, 0xCE, 0xDD, 0x27, + 0x82, 0xD6, 0xD3, 0x37, 0xDA, 0x23, 0xDA, + 0x06, 0x0C, 0x92, 0xD5, 0x4E, 0x9A, 0x53, + 0x05, 0x50, 0x5A, 0x87, 0x7E, 0x06, 0x99, + 0x98, 0x65, 0x06, 0xA5, 0x42 + }, + { + 0x02, 0x35, 0xE7, 0x4D, 0xF6, 0x3C, 0x97, + 0x0E, 0xA8, 0xA3, 0x26, 0x17, 0x47, 0x1B, + 0xB6, 0xA5, 0x94, 0xCB, 0x9A, 0xBB, 0x2E, + 0x63, 0x84, 0x70, 0x8D, 0xE1, 0x93, 0xD7, + 0xD8, 0x3D, 0x04, 0xF5, 0x7D + }, + { + 0x02, 0x99, 0xAF, 0x46, 0x8F, 0x7D, 0x85, + 0x7E, 0x9A, 0x19, 0x47, 0xA6, 0x46, 0xBC, + 0xF6, 0x6B, 0x84, 0xE1, 0x25, 0x24, 0x98, + 0xA1, 0x9C, 0xCF, 0xA4, 0x5B, 0x75, 0xDD, + 0x87, 0x5E, 0xE3, 0x54, 0xB1 + }, + { + 0x03, 0x3E, 0xAD, 0x34, 0x7E, 0x1F, 0x9F, + 0x1C, 0x90, 0xCC, 0xA6, 0xA7, 0x9E, 0x94, + 0x70, 0xC1, 0x46, 0xC5, 0x4F, 0x43, 0x35, + 0xD9, 0xE0, 0x99, 0xF5, 0x4B, 0x25, 0xE5, + 0xF5, 0x97, 0xDD, 0x56, 0x8E + }, + { + 0x02, 0x11, 0xF9, 0x43, 0x86, 0x20, 0x6C, + 0x0A, 0x0A, 0xC8, 0x3A, 0xCC, 0x44, 0xB9, + 0x2C, 0x32, 0x59, 0xB2, 0x33, 0xE7, 0xC5, + 0x26, 0x02, 0x82, 0x6B, 0x27, 0xB0, 0xB3, + 0xDC, 0xF3, 0x9E, 0x0D, 0x69 + }, + { + 0x02, 0x71, 0x37, 0x40, 0x46, 0x3C, 0xDF, + 0x2F, 0xAC, 0x9E, 0x70, 0xB8, 0x90, 0xD4, + 0x30, 0xA8, 0x55, 0x44, 0xBA, 0xC7, 0x01, + 0xE5, 0xFE, 0x86, 0x5B, 0x18, 0x46, 0xF2, + 0xBA, 0xC2, 0xDF, 0x68, 0xE3 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x03, 0xAD, 0x81, 0x3A, 0xA8, 0x13, 0xFC, + 0x44, 0x39, 0xB0, 0x5B, 0xC6, 0x31, 0xA4, + 0xC8, 0x2D, 0x62, 0x57, 0xDD, 0x76, 0xDE, + 0x95, 0x22, 0xF1, 0x32, 0x99, 0x4D, 0xA5, + 0x7B, 0xA9, 0xCC, 0x41, 0x2C, 0x02, 0x8A, + 0xA1, 0x12, 0xC1, 0x38, 0xA7, 0x95, 0xCE, + 0x25, 0x4B, 0xE3, 0xBE, 0x0E, 0x20, 0x36, + 0xE3, 0x32, 0x35, 0x4D, 0xBF, 0x07, 0x26, + 0x8A, 0x35, 0xA0, 0xB8, 0x97, 0xEB, 0x67, + 0x83, 0xC1, 0xC0 + }, + { + 0x03, 0xEB, 0xC3, 0x0F, 0x61, 0x7F, 0x9D, + 0x52, 0x5B, 0xE7, 0xBE, 0xB0, 0xB8, 0x13, + 0x83, 0xE5, 0xE3, 0xB6, 0x72, 0x25, 0x6C, + 0x9B, 0x3D, 0x32, 0x8A, 0x88, 0xB8, 0x40, + 0x1F, 0x2D, 0xDB, 0x00, 0x84, 0x02, 0x4E, + 0x15, 0x0D, 0xD3, 0x0E, 0xE7, 0xBA, 0x33, + 0x7A, 0xB5, 0x57, 0x15, 0x90, 0x58, 0xB6, + 0xC1, 0xB0, 0xED, 0xCD, 0xC1, 0x32, 0x2B, + 0xE7, 0x92, 0x7C, 0xEC, 0xF5, 0x3A, 0x64, + 0xD9, 0x89, 0xCB + }, + { + 0x03, 0x32, 0x57, 0x98, 0xA2, 0x58, 0xC3, + 0x00, 0xB8, 0x39, 0x2A, 0x98, 0x60, 0xFF, + 0xED, 0xB6, 0x4F, 0x23, 0x6F, 0x86, 0x1D, + 0x8C, 0x22, 0xA1, 0x4B, 0x65, 0x9D, 0x4E, + 0xBD, 0xA7, 0xC4, 0xE5, 0x2C, 0x03, 0x48, + 0x1C, 0xC6, 0x39, 0x9A, 0x33, 0x1D, 0xCD, + 0x8A, 0xB7, 0x17, 0x87, 0x05, 0x38, 0xEA, + 0x76, 0x0D, 0xFE, 0x16, 0x8C, 0x08, 0xD5, + 0x3F, 0x7F, 0x9C, 0x96, 0x2C, 0xEA, 0xE0, + 0x56, 0xA5, 0xF0 + }, + { + 0x02, 0xA2, 0x9A, 0x9F, 0x85, 0x38, 0x8C, + 0x9B, 0xDF, 0xA1, 0x2D, 0xF4, 0xE3, 0x46, + 0xB3, 0xD4, 0xE7, 0xFF, 0x6B, 0x3F, 0xD4, + 0x79, 0xB0, 0x34, 0x53, 0x0E, 0x20, 0x0A, + 0x64, 0x64, 0x9D, 0x30, 0x2F, 0x02, 0x32, + 0xB7, 0x32, 0x18, 0x3B, 0x62, 0xEE, 0x6C, + 0xBF, 0x40, 0xBD, 0x32, 0xA0, 0x3F, 0x70, + 0x93, 0x59, 0x0A, 0xA5, 0xFD, 0x75, 0xF1, + 0xFF, 0x55, 0xF8, 0xE8, 0x0F, 0x11, 0x9D, + 0x56, 0xC0, 0x1C + }, + { + 0x03, 0x18, 0x37, 0xE3, 0xE0, 0x47, 0x1A, + 0x93, 0xBA, 0x65, 0xAC, 0xB7, 0x43, 0x0E, + 0xCD, 0x29, 0xD5, 0x70, 0xEC, 0x1D, 0xFE, + 0x5A, 0x4D, 0xAB, 0x32, 0xE7, 0x0F, 0xE2, + 0x0F, 0x1C, 0x28, 0x1A, 0xAC, 0x03, 0x5E, + 0xA1, 0x51, 0x8E, 0x68, 0x43, 0xAF, 0xAE, + 0x01, 0xF3, 0xCC, 0x76, 0x99, 0xAB, 0x87, + 0x06, 0xDE, 0xB5, 0xC8, 0x0A, 0x20, 0xE3, + 0x4A, 0xB5, 0x89, 0x9C, 0xF9, 0xA1, 0x26, + 0xA2, 0xC6, 0x35 + }, + }, + { /* signature shares, the first t participants */ + { + 0x16, 0x50, 0xF8, 0xA4, 0x22, 0x8A, 0x01, + 0x55, 0xBB, 0x1D, 0xB8, 0x61, 0xC9, 0x5B, + 0xB4, 0xFF, 0xC1, 0xBD, 0x53, 0x39, 0xF2, + 0xC1, 0x01, 0x0C, 0x62, 0x8E, 0x9E, 0x8F, + 0x6A, 0xAF, 0x0C, 0xE3 + }, + { + 0x91, 0x47, 0x44, 0xF6, 0xAD, 0x49, 0x79, + 0xC7, 0x1A, 0x8D, 0x1F, 0xA9, 0xBC, 0xAA, + 0xBE, 0xAC, 0xD4, 0xD1, 0x70, 0xB2, 0x00, + 0x2E, 0xEE, 0x14, 0xDF, 0x1E, 0x12, 0x66, + 0xDD, 0x25, 0x55, 0x64 + }, + { + 0x5C, 0x9F, 0x83, 0xF8, 0x34, 0x21, 0x28, + 0xC0, 0x91, 0x01, 0x73, 0xA5, 0x91, 0x3B, + 0x29, 0x30, 0xE4, 0x01, 0x91, 0x0D, 0x43, + 0xAB, 0x4D, 0x8F, 0x10, 0x03, 0x71, 0xCC, + 0xB5, 0xEE, 0x1C, 0xDC + }, + }, + }, + { + "4of7", 7, 4, 7, 35, + { /* one seed per (t-1)-subset, in rank order */ + { + 0xE9, 0x87, 0x56, 0x9D, 0xA4, 0x2D, 0xC0, + 0x91, 0x6E, 0xBC, 0x56, 0x65, 0x94, 0x2F, + 0x96, 0x9B, 0xCD, 0xF5, 0xAD, 0x02, 0xB2, + 0x29, 0xAE, 0x31, 0x52, 0x2C, 0xF3, 0x1E, + 0x0B, 0x26, 0x87, 0x2E + }, + { + 0xBB, 0x75, 0x2D, 0xD0, 0x64, 0x6A, 0x11, + 0x0F, 0xA5, 0x8D, 0x7B, 0xFA, 0x59, 0xF0, + 0xF8, 0xCB, 0xD0, 0x9D, 0x8B, 0x86, 0x47, + 0x1C, 0x5E, 0x12, 0x2D, 0xFD, 0xD4, 0x6E, + 0xF8, 0xD6, 0x89, 0x8C + }, + { + 0xF8, 0xB2, 0xD5, 0xA8, 0xE2, 0xD5, 0xD7, + 0x67, 0x43, 0xA6, 0x1A, 0x68, 0xF8, 0x02, + 0x6F, 0xB2, 0xC0, 0x19, 0xFB, 0xDC, 0xAC, + 0x8F, 0x71, 0x9F, 0xE8, 0x24, 0xFE, 0x97, + 0xA9, 0x31, 0xC6, 0x39 + }, + { + 0x06, 0x50, 0xF6, 0xD2, 0x7E, 0x3D, 0x22, + 0x7C, 0xBD, 0xAD, 0x81, 0xB2, 0x94, 0xDB, + 0x5F, 0xA0, 0x9B, 0xC5, 0x81, 0x15, 0xCB, + 0x90, 0xCF, 0xBA, 0x9A, 0x2D, 0x63, 0x0A, + 0xD8, 0x11, 0xD9, 0xF2 + }, + { + 0x6A, 0x1E, 0x3A, 0xF4, 0x69, 0xB1, 0x99, + 0xCC, 0xF9, 0x21, 0x37, 0x16, 0x0E, 0x3B, + 0xCB, 0x9D, 0x65, 0x04, 0x46, 0xC2, 0x33, + 0x23, 0x87, 0xBF, 0x9C, 0x63, 0x41, 0x6C, + 0x9D, 0x65, 0xE8, 0xB2 + }, + { + 0xE9, 0x00, 0x04, 0x2E, 0x1A, 0xC9, 0x82, + 0xD6, 0xC6, 0xA7, 0x38, 0x7B, 0xAB, 0xBA, + 0xD6, 0x16, 0xDB, 0x0F, 0x62, 0x70, 0xFE, + 0x6A, 0xF6, 0x26, 0x0D, 0x06, 0x7E, 0x69, + 0x6E, 0x72, 0x61, 0x1B + }, + { + 0xB0, 0xCF, 0x62, 0x36, 0xA4, 0x28, 0x0A, + 0x85, 0xEB, 0x57, 0x2B, 0xE7, 0x59, 0xFF, + 0xDA, 0x80, 0xBE, 0x11, 0x99, 0x94, 0xEB, + 0x89, 0x3A, 0x66, 0x82, 0x17, 0xC9, 0xF9, + 0x52, 0x7B, 0xE9, 0x73 + }, + { + 0x73, 0x14, 0x6D, 0x2B, 0x7C, 0x38, 0x07, + 0x04, 0xB5, 0xD1, 0x33, 0x02, 0x87, 0xF7, + 0x69, 0x29, 0xEB, 0xBC, 0xBD, 0x67, 0x16, + 0x1D, 0x8F, 0x2C, 0x92, 0x86, 0x6E, 0xB4, + 0xE2, 0xC5, 0xAC, 0x4A + }, + { + 0x96, 0xF8, 0x89, 0xB2, 0x71, 0x65, 0x24, + 0x96, 0x14, 0x64, 0x64, 0xFD, 0xF5, 0x87, + 0x0C, 0x09, 0xDF, 0xD2, 0x95, 0xD0, 0x54, + 0x70, 0xCF, 0xB9, 0xA7, 0xED, 0xFA, 0xC1, + 0x80, 0x70, 0x97, 0xAF + }, + { + 0x91, 0xC1, 0xC1, 0x0B, 0x06, 0x5A, 0x10, + 0x78, 0xB9, 0x30, 0x78, 0xB9, 0x0B, 0x90, + 0xB9, 0x4F, 0x48, 0xBB, 0xEE, 0x39, 0xE1, + 0x05, 0xE5, 0x17, 0x2C, 0xA4, 0x94, 0x4D, + 0x3A, 0x72, 0xAD, 0xAD + }, + { + 0x99, 0x2E, 0x01, 0xDF, 0x39, 0x2A, 0xCE, + 0xA5, 0xFF, 0x9A, 0x74, 0x89, 0xB5, 0x64, + 0x39, 0xFC, 0xF7, 0xD8, 0xD6, 0xD6, 0xC9, + 0x85, 0x74, 0xA4, 0xA5, 0x0D, 0xAA, 0xA7, + 0xD1, 0x83, 0x7E, 0xB1 + }, + { + 0x5F, 0xF6, 0xF6, 0xEA, 0x60, 0x70, 0x98, + 0xA1, 0x73, 0xEA, 0xBF, 0xC5, 0x48, 0xCE, + 0x26, 0x09, 0x03, 0x69, 0xCF, 0x80, 0xA2, + 0xFE, 0x29, 0xA1, 0x0A, 0x51, 0x20, 0x39, + 0xC3, 0xBD, 0x20, 0x3E + }, + { + 0x92, 0x2B, 0x2B, 0xD9, 0x90, 0x5B, 0x3B, + 0x44, 0x69, 0xAD, 0x5C, 0xD9, 0x6F, 0x89, + 0x9E, 0x92, 0xC3, 0xA8, 0xB9, 0x9D, 0xCE, + 0x3F, 0xEA, 0x11, 0x63, 0xE1, 0x2C, 0xC7, + 0x55, 0xC1, 0x8B, 0xCE + }, + { + 0x4A, 0xC9, 0xA2, 0x2E, 0x8E, 0x69, 0x3F, + 0xB8, 0x3D, 0x34, 0x7C, 0x22, 0x1B, 0x7E, + 0x79, 0x51, 0x50, 0xD4, 0xF5, 0x34, 0x47, + 0x74, 0x02, 0x09, 0x3F, 0x01, 0x3A, 0xC9, + 0x4B, 0xAC, 0xE7, 0xB6 + }, + { + 0x5F, 0x8B, 0x9A, 0xD4, 0x69, 0xCF, 0xF7, + 0xE2, 0xA5, 0x43, 0x22, 0xAE, 0x33, 0xA5, + 0xFE, 0x8A, 0x16, 0x67, 0xA8, 0x85, 0xC1, + 0xAC, 0x6C, 0x29, 0x31, 0xB1, 0x10, 0x6D, + 0x89, 0x68, 0x50, 0x21 + }, + { + 0xC3, 0xF5, 0xB5, 0x09, 0x59, 0x9B, 0x79, + 0x64, 0x09, 0x03, 0x69, 0x35, 0x38, 0x01, + 0xCB, 0xB1, 0xCD, 0x31, 0x31, 0x0A, 0x61, + 0x0E, 0x6E, 0x5B, 0x83, 0x2D, 0xFB, 0x78, + 0xF7, 0xCA, 0x1A, 0x8D + }, + { + 0x4C, 0xFE, 0xE9, 0x2A, 0x8B, 0x74, 0xE2, + 0xA8, 0x73, 0x98, 0x68, 0xE9, 0xFE, 0x41, + 0x65, 0xC9, 0xA1, 0xED, 0x4D, 0x9F, 0xFC, + 0xBC, 0xE9, 0xA5, 0x4C, 0x55, 0x2D, 0x9C, + 0x55, 0x3C, 0xBD, 0xDE + }, + { + 0x2A, 0xD5, 0x0C, 0x10, 0x0A, 0xAE, 0x0D, + 0xE3, 0x7E, 0xC9, 0xF6, 0x57, 0x9A, 0xB3, + 0xAD, 0x28, 0x2E, 0x39, 0x6D, 0xC6, 0x1A, + 0xDC, 0x9E, 0xFA, 0x02, 0xD7, 0x06, 0x24, + 0x08, 0xF6, 0x7A, 0x63 + }, + { + 0x41, 0xD9, 0x79, 0x9E, 0x0F, 0x3C, 0x98, + 0xFD, 0x5B, 0xE8, 0xEA, 0x52, 0x3B, 0x2E, + 0x05, 0x35, 0x7A, 0xB6, 0x12, 0x67, 0x61, + 0x33, 0x6D, 0x9C, 0x10, 0xA5, 0x46, 0x56, + 0x42, 0xF7, 0x12, 0x5A + }, + { + 0x3B, 0xC9, 0x8F, 0xF6, 0x11, 0xF8, 0xA7, + 0x2D, 0xC6, 0xCB, 0x15, 0xCF, 0x68, 0xD8, + 0xBB, 0xA4, 0xCC, 0xCE, 0xDC, 0x1D, 0x50, + 0x70, 0x82, 0x37, 0x85, 0x51, 0xEE, 0xA4, + 0xCA, 0x9D, 0x26, 0x42 + }, + { + 0x4A, 0x49, 0xD2, 0x89, 0xF1, 0x51, 0x8D, + 0x82, 0xF8, 0x64, 0x33, 0xD9, 0x11, 0x08, + 0xDE, 0x5E, 0x53, 0xAB, 0x1C, 0xAF, 0x5D, + 0x71, 0x97, 0x87, 0xC9, 0xB9, 0x36, 0xCD, + 0x6B, 0x26, 0x2B, 0xE0 + }, + { + 0x0D, 0xC9, 0xCD, 0x73, 0xDD, 0xF5, 0x8B, + 0x40, 0xAF, 0xB6, 0x70, 0x0B, 0x56, 0x78, + 0x69, 0x18, 0x5A, 0x02, 0xA8, 0x7D, 0x2B, + 0x88, 0x83, 0x45, 0x91, 0x65, 0x7D, 0x71, + 0xA2, 0x25, 0x76, 0x6F + }, + { + 0xC6, 0x4D, 0x54, 0x72, 0x00, 0x43, 0xC5, + 0xDF, 0x01, 0x7D, 0xBC, 0xA8, 0xDB, 0xAA, + 0xBE, 0x1A, 0xB3, 0x05, 0x65, 0xEF, 0x2A, + 0xDA, 0xEA, 0xEC, 0x2E, 0xAB, 0xB9, 0x97, + 0xDC, 0x48, 0x2C, 0x61 + }, + { + 0xE5, 0x09, 0x06, 0x0D, 0x75, 0x7C, 0xCF, + 0x38, 0x10, 0xF7, 0xDE, 0x5A, 0x13, 0x1C, + 0xE8, 0x95, 0x6E, 0xA0, 0x45, 0x5E, 0x60, + 0x4F, 0x94, 0x73, 0x49, 0x18, 0x26, 0xBC, + 0xF3, 0x45, 0x2C, 0xE0 + }, + { + 0xBD, 0xA7, 0xFE, 0x41, 0x7A, 0x4F, 0x2B, + 0x1B, 0xD5, 0x50, 0x88, 0xA4, 0xB7, 0x19, + 0x11, 0xFC, 0x1F, 0x9B, 0x67, 0xD3, 0x00, + 0x0F, 0x27, 0xB5, 0xC7, 0xA3, 0xD2, 0x77, + 0x29, 0xA4, 0x65, 0x1C + }, + { + 0x29, 0x64, 0xFD, 0xFC, 0xCB, 0x6B, 0x6D, + 0xB7, 0xD4, 0x3A, 0x5F, 0x29, 0x7A, 0x44, + 0x5C, 0x3D, 0x08, 0x83, 0x8A, 0x63, 0xA3, + 0x88, 0x4B, 0x07, 0x56, 0xFC, 0x29, 0x1B, + 0x8E, 0x98, 0xD6, 0x99 + }, + { + 0x3F, 0xFB, 0x31, 0x66, 0x44, 0xB2, 0x86, + 0x15, 0x3C, 0x39, 0xBF, 0x9E, 0x90, 0xA4, + 0x71, 0x03, 0xF8, 0x7D, 0x5D, 0x54, 0x3A, + 0x42, 0x8D, 0x5C, 0x50, 0xBA, 0x51, 0x2F, + 0x15, 0xB7, 0x3A, 0x4D + }, + { + 0x4B, 0x12, 0x83, 0x94, 0x39, 0xAB, 0xA7, + 0x22, 0xC3, 0x92, 0xA4, 0x5C, 0x38, 0xF2, + 0x0C, 0x2A, 0xBF, 0xC2, 0x11, 0xF2, 0x2B, + 0x5F, 0xCC, 0x6D, 0x88, 0xDF, 0x17, 0x8F, + 0xBD, 0x19, 0x7A, 0x52 + }, + { + 0xC5, 0x65, 0x84, 0xE7, 0x0C, 0xF3, 0x3F, + 0x57, 0xA4, 0xF9, 0x07, 0x03, 0xC1, 0x02, + 0x86, 0x0A, 0x31, 0xAC, 0xBF, 0xA3, 0x15, + 0x7A, 0x1D, 0xF4, 0x34, 0x0F, 0xAE, 0x35, + 0x8D, 0xBF, 0x2E, 0x72 + }, + { + 0xCE, 0x52, 0x0D, 0x74, 0x54, 0xD1, 0x60, + 0x9C, 0x36, 0xAA, 0x79, 0x3A, 0x61, 0xCB, + 0xE0, 0x30, 0xAA, 0xD9, 0xBE, 0x80, 0x75, + 0x27, 0xFC, 0x35, 0x46, 0xAA, 0xAF, 0xFE, + 0xFC, 0x0E, 0x8C, 0x93 + }, + { + 0xB3, 0x2D, 0xBA, 0xDF, 0x25, 0x0F, 0x84, + 0x27, 0xEB, 0xBE, 0xAB, 0x82, 0x60, 0x92, + 0x9E, 0x26, 0x2A, 0xE1, 0xE4, 0xB9, 0x07, + 0x6D, 0x06, 0xB3, 0x6C, 0x61, 0x18, 0xB8, + 0x01, 0x95, 0x64, 0xB0 + }, + { + 0xEA, 0xD5, 0x8E, 0xE0, 0x0B, 0xB0, 0x00, + 0x78, 0x68, 0x6F, 0x55, 0x3F, 0xED, 0xD1, + 0x17, 0x32, 0x80, 0x78, 0x07, 0xC2, 0xC2, + 0x66, 0xC1, 0x96, 0xC8, 0xB0, 0xEE, 0x2C, + 0x2F, 0x9E, 0x6D, 0xB4 + }, + { + 0xBB, 0xA6, 0x29, 0xA1, 0x56, 0x59, 0x5F, + 0xC3, 0x6C, 0x5D, 0xC6, 0xD7, 0xC1, 0xD7, + 0x82, 0x41, 0x28, 0xD1, 0xDF, 0xFA, 0x72, + 0x32, 0xF0, 0xFB, 0xFC, 0x7B, 0x88, 0x21, + 0xE4, 0xD0, 0x50, 0x5A + }, + { + 0xB6, 0x79, 0x6C, 0x04, 0xEB, 0x61, 0xC4, + 0xF2, 0xB3, 0xA7, 0xEE, 0x42, 0x2B, 0x56, + 0xC7, 0x26, 0x10, 0x01, 0x94, 0xC4, 0x51, + 0xF2, 0xDD, 0x4E, 0x85, 0xBF, 0x2B, 0x34, + 0xA8, 0xE5, 0x7E, 0xA2 + }, + { + 0x49, 0xAC, 0xF7, 0xC8, 0xFE, 0xE6, 0x90, + 0xD0, 0xCB, 0x0F, 0x86, 0x29, 0x1F, 0x7A, + 0xDC, 0xA4, 0x58, 0x40, 0x36, 0x21, 0x05, + 0x60, 0xD0, 0xBE, 0x70, 0x6B, 0x19, 0x84, + 0xD9, 0xDD, 0xD2, 0x81 + }, + }, + { /* group_pk */ + 0x02, 0xEF, 0x23, 0x01, 0xB1, 0x3A, 0xCE, 0x73, 0x05, + 0x31, 0x76, 0xD2, 0x66, 0x14, 0xA8, 0x4C, 0xD5, 0x14, + 0xF2, 0xBB, 0x5E, 0x70, 0x98, 0xDD, 0xEB, 0xF3, 0x5D, + 0xAE, 0x5F, 0x99, 0x1B, 0xB5, 0x91 + }, + { /* msg */ + 0x8B, 0x4C, 0x7E, 0xC2, 0xA0, 0x7F, 0x68, 0x74, 0xB5, + 0xDB, 0xE7, 0x82, 0x38, 0xED, 0xAB, 0x02, 0x72, 0xAB, + 0x01, 0x61, 0xC6, 0x2B, 0xF5, 0xF6, 0x4A, 0xE7, 0xEA, + 0x2C, 0xBD, 0x77, 0x33, 0x21 + }, + { /* sid */ + 0x5B, 0xB9, 0xD8, 0xF8, 0x87, 0xFD, 0x3D, 0x52, 0xB6, + 0xBA, 0x1B, 0x5F, 0x78, 0x03, 0x3E, 0xEA, 0xE8, 0x29, + 0x1D, 0xF9, 0xBE, 0x21, 0x96, 0x95, 0x34, 0x24, 0x55, + 0xAE, 0xD6, 0x81, 0x2A, 0x82 + }, + { /* cosigner_pk */ + 0x02, 0xEA, 0x72, 0xD7, 0x97, 0x42, 0xF5, 0x35, 0x5F, + 0xF8, 0x3C, 0x4C, 0x96, 0xDD, 0xC1, 0xE7, 0xBD, 0x35, + 0x47, 0x4D, 0x85, 0x97, 0x62, 0x12, 0xE7, 0xFE, 0xA6, + 0x32, 0x63, 0x36, 0xE3, 0x4E, 0x93 + }, + { /* cosigner_pubnonce */ + 0x03, 0xA5, 0xC3, 0xD1, 0x66, 0x24, 0x48, 0xD9, 0xCC, + 0xCA, 0x44, 0x5F, 0xAE, 0x11, 0xD0, 0xD1, 0x14, 0x45, + 0xAA, 0x69, 0x32, 0xAF, 0x75, 0xA0, 0xE0, 0xF7, 0x6C, + 0x7B, 0x62, 0xE3, 0x35, 0xD1, 0xD2, 0x02, 0x98, 0x92, + 0xFE, 0x0B, 0x32, 0x9A, 0x7C, 0xCD, 0x0E, 0x95, 0x83, + 0x4A, 0xFB, 0xD8, 0xA2, 0x3B, 0x63, 0x9E, 0xDC, 0x0D, + 0xAC, 0xC9, 0x0C, 0xA1, 0xC6, 0x39, 0x7A, 0x30, 0x6E, + 0x9E, 0x64, 0x19 + }, + { /* group_aggnonce */ + 0x02, 0x23, 0x8A, 0x7D, 0x69, 0x2B, 0xED, 0x3E, 0x8B, + 0xBF, 0xF3, 0x70, 0x00, 0x84, 0xCF, 0x31, 0x2E, 0x4A, + 0x68, 0xC9, 0x43, 0xBD, 0x4D, 0xAC, 0x49, 0xDE, 0x5E, + 0xC0, 0xE0, 0xB9, 0x80, 0x04, 0x04, 0x03, 0x3E, 0x4B, + 0x5F, 0xE5, 0x83, 0xE4, 0xFA, 0x27, 0xD8, 0xE1, 0x31, + 0xEF, 0xAA, 0x9B, 0x53, 0xA3, 0x5B, 0x2B, 0x4E, 0x4A, + 0xF3, 0x2A, 0xAF, 0xFA, 0x2C, 0xD4, 0x4B, 0x7C, 0x65, + 0x91, 0x53, 0xE0 + }, + { /* group_pubnonce */ + 0x02, 0x23, 0x8A, 0x7D, 0x69, 0x2B, 0xED, 0x3E, 0x8B, + 0xBF, 0xF3, 0x70, 0x00, 0x84, 0xCF, 0x31, 0x2E, 0x4A, + 0x68, 0xC9, 0x43, 0xBD, 0x4D, 0xAC, 0x49, 0xDE, 0x5E, + 0xC0, 0xE0, 0xB9, 0x80, 0x04, 0x04, 0x02, 0x64, 0x37, + 0x8F, 0xFC, 0x31, 0xA6, 0x60, 0x21, 0x14, 0x3C, 0xAC, + 0x54, 0x11, 0xF5, 0xB6, 0xCF, 0xD2, 0x3A, 0xD6, 0x00, + 0x06, 0x7E, 0x7D, 0xE6, 0x50, 0xE0, 0x0B, 0xCA, 0xB4, + 0x7F, 0x8E, 0x92 + }, + { /* group_psig */ + 0x33, 0x52, 0xF6, 0x5D, 0x39, 0x6E, 0xBE, 0xF7, 0x31, + 0x90, 0xA9, 0x35, 0x4C, 0x85, 0xC6, 0x24, 0x90, 0x62, + 0xB3, 0xE2, 0x42, 0x6B, 0x6D, 0xD3, 0x7C, 0x16, 0x1D, + 0x77, 0xA1, 0xCE, 0x9F, 0x48 + }, + { /* pubshares, participant 1 first */ + { + 0x03, 0xCF, 0x1A, 0xCF, 0x20, 0x52, 0x24, + 0xA3, 0xF2, 0xC1, 0xDA, 0xA9, 0xB8, 0x18, + 0xCB, 0x19, 0x40, 0x7A, 0x57, 0x5A, 0xDB, + 0xA6, 0xBA, 0xF3, 0xBF, 0x98, 0x61, 0xD0, + 0x4E, 0xF1, 0xBB, 0xA1, 0x61 + }, + { + 0x02, 0x06, 0x41, 0x0D, 0xB9, 0xF0, 0x56, + 0x40, 0x0C, 0x87, 0xCC, 0xA1, 0x6E, 0x64, + 0x33, 0x56, 0x1D, 0x97, 0x26, 0x8E, 0x41, + 0xBB, 0x8F, 0xE5, 0x9C, 0x10, 0x62, 0x25, + 0x01, 0x01, 0x54, 0xEA, 0x5E + }, + { + 0x03, 0x99, 0xFE, 0xA6, 0x89, 0xA7, 0x76, + 0x3C, 0x56, 0xAB, 0xD1, 0x6F, 0x09, 0x10, + 0x6C, 0x98, 0xBB, 0xE1, 0x5D, 0x7C, 0x24, + 0xCC, 0xFA, 0x62, 0x1D, 0xE1, 0x41, 0x56, + 0x9D, 0x7D, 0x6F, 0x3D, 0x8C + }, + { + 0x03, 0xF3, 0xE2, 0x1D, 0x56, 0xB8, 0x58, + 0xF7, 0xEC, 0xB2, 0xFC, 0xDD, 0xE9, 0xBD, + 0x8E, 0xF3, 0x11, 0xFC, 0x7A, 0x30, 0x1D, + 0xC5, 0xC7, 0x93, 0x38, 0xE3, 0x33, 0xE6, + 0xB7, 0xB3, 0x7B, 0x30, 0x59 + }, + { + 0x02, 0xD2, 0xD0, 0x3A, 0xA2, 0x7D, 0xA0, + 0xDC, 0x27, 0x4B, 0x9E, 0x5C, 0x38, 0x82, + 0xEB, 0x56, 0xE6, 0xFF, 0xFA, 0xA2, 0x93, + 0x28, 0x06, 0x29, 0x59, 0xC5, 0x40, 0x12, + 0xD0, 0xE0, 0xD3, 0xA7, 0x91 + }, + { + 0x03, 0x7C, 0xAD, 0x0A, 0x4E, 0xDE, 0x9B, + 0x40, 0xCB, 0x01, 0x05, 0x6E, 0xE5, 0xC4, + 0xCB, 0x2F, 0x0B, 0xC4, 0xBF, 0x3D, 0xF9, + 0x15, 0x2A, 0x10, 0xBA, 0x86, 0x15, 0x3A, + 0xC6, 0x11, 0xA1, 0xA6, 0x49 + }, + { + 0x02, 0xDA, 0xC2, 0x7D, 0x4C, 0xE5, 0x79, + 0x0A, 0xEC, 0xDF, 0x3D, 0xA1, 0x4D, 0xB3, + 0x6B, 0xD3, 0xF3, 0x51, 0x02, 0x21, 0x21, + 0x6B, 0x2D, 0xA1, 0xA6, 0x53, 0x41, 0xE8, + 0x96, 0xFC, 0x2F, 0x7F, 0x86 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x02, 0xC3, 0x83, 0xC7, 0xC1, 0x70, 0xC4, + 0x97, 0x8E, 0x73, 0x35, 0x4C, 0x45, 0x6B, + 0x62, 0x37, 0x24, 0x93, 0xFF, 0x8A, 0x7E, + 0x7B, 0xEB, 0x1E, 0x94, 0xFE, 0xD8, 0xEE, + 0x7A, 0x3A, 0x85, 0x41, 0x03, 0x03, 0x34, + 0x99, 0x00, 0x67, 0xB5, 0x4F, 0x7D, 0x2E, + 0x33, 0x62, 0x48, 0x44, 0xA7, 0x57, 0x7E, + 0x41, 0x01, 0xD4, 0x49, 0x34, 0x18, 0x31, + 0x59, 0xD3, 0xE8, 0xCA, 0x0B, 0x3D, 0xEF, + 0x63, 0x70, 0x43 + }, + { + 0x02, 0xCD, 0x94, 0x06, 0x1E, 0x3A, 0x78, + 0xD7, 0x4C, 0xE1, 0x27, 0xD1, 0x7E, 0x03, + 0x6C, 0x0B, 0xF4, 0x0C, 0xA3, 0x9C, 0xFD, + 0x5B, 0x6A, 0x44, 0x21, 0xC6, 0x28, 0x69, + 0x04, 0x61, 0xDC, 0x6E, 0xEA, 0x03, 0x3D, + 0x26, 0x45, 0x8A, 0x05, 0xC2, 0x90, 0xE9, + 0x15, 0x9D, 0xFC, 0x34, 0x60, 0x34, 0x90, + 0x12, 0xB1, 0x7E, 0x70, 0x23, 0xAD, 0x66, + 0xC6, 0x26, 0x35, 0x47, 0x7C, 0xE4, 0x06, + 0xB1, 0xFB, 0xB8 + }, + { + 0x02, 0x5F, 0x29, 0x58, 0xDC, 0xF5, 0xD2, + 0xC6, 0x8D, 0xA3, 0x0C, 0x58, 0x60, 0xD7, + 0xDA, 0xF5, 0x02, 0x29, 0x0B, 0xCA, 0xB6, + 0x4F, 0xE4, 0xA8, 0x2B, 0x96, 0x7E, 0xFC, + 0x18, 0x08, 0x64, 0x86, 0xB0, 0x02, 0x9B, + 0xBF, 0x4D, 0x31, 0x98, 0x74, 0x49, 0x91, + 0x40, 0x49, 0x6C, 0x26, 0x22, 0xCA, 0xA8, + 0x38, 0x0F, 0x3B, 0x18, 0xA0, 0x47, 0x22, + 0xCD, 0x57, 0x32, 0x71, 0x8E, 0xB4, 0x07, + 0x19, 0x13, 0x3D + }, + { + 0x02, 0x6A, 0x11, 0xE1, 0x13, 0xCB, 0xB2, + 0x33, 0x88, 0x8E, 0xD7, 0xD5, 0xC3, 0x3F, + 0xA6, 0x18, 0x82, 0x37, 0x4D, 0xAD, 0xE1, + 0x89, 0x92, 0x96, 0xFA, 0x09, 0x4E, 0x2A, + 0x99, 0x53, 0x47, 0xDF, 0xDD, 0x02, 0xD0, + 0x04, 0x8D, 0x3D, 0xEF, 0x11, 0xC1, 0x95, + 0x89, 0xB6, 0x1A, 0xD9, 0xD0, 0x3B, 0xB7, + 0xBC, 0x1A, 0x66, 0xE5, 0x85, 0x53, 0x45, + 0x9C, 0xE0, 0x9F, 0xEA, 0xFC, 0xED, 0xA1, + 0x01, 0x6B, 0xCB + }, + { + 0x02, 0xB4, 0x22, 0x40, 0x46, 0xAE, 0x58, + 0x27, 0x50, 0x80, 0x26, 0xCC, 0xD8, 0x21, + 0x75, 0x47, 0x22, 0x5A, 0xB3, 0x61, 0x5A, + 0xC2, 0x57, 0x3D, 0x7F, 0xD8, 0xA2, 0x7E, + 0xDF, 0x20, 0xAD, 0x40, 0x0D, 0x02, 0x39, + 0x08, 0x3E, 0xEA, 0xCE, 0x02, 0xF4, 0x2F, + 0xA4, 0x2B, 0xED, 0xE3, 0x32, 0xC8, 0xB3, + 0xCC, 0x5A, 0x82, 0x63, 0xD0, 0xF4, 0x7C, + 0xC7, 0x0F, 0x44, 0x66, 0xE1, 0xF6, 0x9D, + 0x4C, 0x56, 0x2A + }, + { + 0x02, 0x85, 0x4B, 0x4B, 0x39, 0x28, 0x5A, + 0x09, 0xDA, 0xAE, 0x55, 0x51, 0x8F, 0xFA, + 0x95, 0x2E, 0x2F, 0xBB, 0x76, 0x75, 0x7F, + 0x53, 0xA0, 0x31, 0x99, 0xAB, 0xF2, 0xB0, + 0xD6, 0x60, 0x58, 0xDC, 0xA5, 0x02, 0x17, + 0x4B, 0x4C, 0x61, 0x41, 0xAD, 0xDF, 0x0E, + 0x0A, 0xDA, 0xAB, 0xC9, 0xC9, 0xE6, 0x29, + 0x5D, 0xB8, 0x17, 0xD7, 0x65, 0x48, 0xFF, + 0x29, 0x92, 0x3F, 0xEF, 0x68, 0x1C, 0xD4, + 0x4B, 0x03, 0x60 + }, + { + 0x02, 0x90, 0x6C, 0x68, 0x5A, 0xA6, 0x1A, + 0xB5, 0x79, 0x6F, 0x92, 0x80, 0xFD, 0xBE, + 0xC0, 0xD9, 0x31, 0x07, 0x66, 0xF1, 0xC6, + 0xEC, 0x5F, 0x24, 0xA4, 0x12, 0x50, 0x68, + 0x88, 0x3B, 0x36, 0xAF, 0xCC, 0x02, 0xD6, + 0x5C, 0x81, 0x28, 0x21, 0x21, 0xDC, 0xA4, + 0x04, 0x95, 0xA7, 0x1F, 0xDA, 0x8D, 0x71, + 0x05, 0x2C, 0x3A, 0xFD, 0x4B, 0xA9, 0xEA, + 0x9F, 0x4C, 0x3B, 0x13, 0xB8, 0x2E, 0xBF, + 0xEE, 0x98, 0xE5 + }, + }, + { /* signature shares, the first t participants */ + { + 0x77, 0xD0, 0x8A, 0x2E, 0x74, 0x58, 0x2A, + 0xD1, 0xA2, 0x38, 0x38, 0xF4, 0xA9, 0x7D, + 0x69, 0xFD, 0x51, 0x07, 0x33, 0xB7, 0x21, + 0x61, 0xE5, 0x0F, 0xA0, 0x85, 0x70, 0x49, + 0xD9, 0x0E, 0xDA, 0x4B + }, + { + 0x44, 0x91, 0x72, 0xAA, 0x78, 0x6F, 0xD7, + 0xBF, 0xD0, 0x24, 0x1E, 0x35, 0x74, 0xB8, + 0x0B, 0x62, 0xA9, 0xAF, 0x0A, 0xC2, 0x38, + 0xA2, 0xB6, 0x74, 0x6E, 0x6A, 0x2C, 0x39, + 0xA1, 0x03, 0x66, 0x58 + }, + { + 0xE7, 0x84, 0x47, 0xCA, 0xA6, 0x6E, 0x37, + 0x1E, 0xB2, 0x81, 0x42, 0xA9, 0xA1, 0xDA, + 0xE8, 0xDF, 0x34, 0x0F, 0xB9, 0xC3, 0x18, + 0x8E, 0xCE, 0x3D, 0x97, 0x2D, 0xF0, 0x89, + 0xFE, 0x16, 0x6F, 0xAC + }, + { + 0xAE, 0x97, 0xA1, 0x88, 0x5F, 0x0B, 0xBA, + 0x4B, 0x40, 0x7C, 0x90, 0x03, 0x24, 0x8B, + 0x41, 0x01, 0x59, 0xD2, 0x2A, 0xC5, 0x43, + 0xAD, 0x37, 0xF3, 0x8C, 0xC3, 0x40, 0xD7, + 0x84, 0x0F, 0x5E, 0xC1 + }, + }, + }, + { + "5of9", 9, 5, 9, 126, + { /* one seed per (t-1)-subset, in rank order */ + { + 0x23, 0x0E, 0x9B, 0x33, 0xB2, 0x85, 0x32, + 0xDA, 0x36, 0x93, 0xD9, 0x25, 0xDD, 0x97, + 0xB8, 0x18, 0xF1, 0x4B, 0x3B, 0x64, 0x3C, + 0x87, 0xB4, 0xA4, 0xD5, 0x4F, 0x53, 0xC8, + 0xEA, 0xF7, 0x53, 0x0C + }, + { + 0x60, 0xE3, 0xEA, 0xC8, 0x7F, 0x2C, 0x66, + 0x62, 0xE7, 0xD8, 0xAA, 0x81, 0x2D, 0x4F, + 0x4D, 0x2D, 0xBB, 0x22, 0x94, 0x06, 0x61, + 0xD7, 0x1B, 0x7F, 0x99, 0xC8, 0xDE, 0xF2, + 0x3B, 0x8A, 0x14, 0x45 + }, + { + 0x9F, 0x55, 0x02, 0xF0, 0xD3, 0x26, 0x5C, + 0xEA, 0x8E, 0x87, 0xEB, 0x01, 0x22, 0x0F, + 0x7E, 0x8C, 0x8C, 0x88, 0x27, 0x1B, 0xF0, + 0x4E, 0x15, 0x51, 0x1E, 0xB5, 0x4B, 0x78, + 0x5B, 0xD9, 0xE0, 0xA6 + }, + { + 0x17, 0x98, 0x3A, 0x0E, 0x99, 0xFB, 0x07, + 0x94, 0xDA, 0x78, 0x71, 0x98, 0xD2, 0x5E, + 0xDD, 0x79, 0xD9, 0xC7, 0xD8, 0xE3, 0x2C, + 0x2D, 0xB8, 0xB8, 0x35, 0x25, 0xE7, 0x2A, + 0x0D, 0xDE, 0x82, 0x8A + }, + { + 0x49, 0x9D, 0x33, 0x12, 0x2E, 0x61, 0xAB, + 0xC6, 0x62, 0xA0, 0x1D, 0xE6, 0x38, 0xED, + 0xAD, 0xF2, 0x9E, 0x43, 0xF7, 0x37, 0x10, + 0x7F, 0x07, 0x20, 0x6A, 0xC0, 0xF3, 0x5A, + 0x6B, 0xD2, 0xE3, 0x6F + }, + { + 0x62, 0xA7, 0x03, 0xB0, 0x02, 0x3F, 0xCB, + 0x0A, 0xC5, 0x86, 0x7D, 0xC5, 0x94, 0x82, + 0x82, 0x32, 0xB8, 0x3C, 0xA0, 0xC8, 0xB9, + 0xE2, 0x3C, 0xB2, 0x1F, 0x0D, 0x1B, 0x94, + 0x85, 0x6F, 0x43, 0x02 + }, + { + 0xA7, 0x2D, 0x69, 0xA6, 0x2F, 0x45, 0x62, + 0xC3, 0xF4, 0x5E, 0x65, 0x83, 0x64, 0x5B, + 0x43, 0x56, 0x34, 0x91, 0x8F, 0x40, 0xFB, + 0xBE, 0x6A, 0xF0, 0x0F, 0x8F, 0x1F, 0x8C, + 0x9A, 0x28, 0x1F, 0xE7 + }, + { + 0x02, 0xF3, 0xA6, 0x01, 0x28, 0xBD, 0x1C, + 0xD1, 0xC6, 0x9A, 0xB6, 0x23, 0x86, 0x8C, + 0x61, 0x51, 0x35, 0x1A, 0xF9, 0x32, 0xFE, + 0xD6, 0x69, 0x4F, 0x1C, 0x99, 0x8A, 0xBD, + 0x78, 0xC0, 0xBB, 0xC4 + }, + { + 0x29, 0x8B, 0x8E, 0x02, 0xBB, 0x26, 0x89, + 0x98, 0xF1, 0x49, 0x79, 0x92, 0x63, 0xFF, + 0xE6, 0xDF, 0x3E, 0x6C, 0x7C, 0xB8, 0x0C, + 0xE6, 0x22, 0x2B, 0x11, 0x4D, 0xE8, 0x8C, + 0x9B, 0x45, 0x31, 0x7A + }, + { + 0x59, 0x98, 0x73, 0x79, 0xCA, 0xC0, 0xC8, + 0x52, 0xF2, 0x8D, 0x01, 0x0E, 0x73, 0x06, + 0x04, 0x2F, 0x7C, 0xBE, 0x03, 0x20, 0x61, + 0xA8, 0xD7, 0x81, 0x32, 0x92, 0x14, 0xD3, + 0x45, 0xCF, 0xC9, 0x54 + }, + { + 0x93, 0xFE, 0xE9, 0xEB, 0xE0, 0x44, 0xA5, + 0xAC, 0x5A, 0xA7, 0x9B, 0xDE, 0x44, 0xDB, + 0x58, 0xBB, 0x68, 0x52, 0x03, 0xA6, 0x02, + 0x5C, 0x8C, 0xD1, 0xE1, 0x18, 0x93, 0x1D, + 0x1B, 0x44, 0x6C, 0xFD + }, + { + 0xE4, 0x46, 0xAE, 0x9C, 0x96, 0xF8, 0xE1, + 0x2B, 0xC8, 0x9E, 0xA4, 0x3B, 0x73, 0x3C, + 0x3E, 0x08, 0x49, 0x7D, 0x01, 0x68, 0xD4, + 0xA5, 0x38, 0x2D, 0x6C, 0xEA, 0x09, 0xE1, + 0x24, 0x7C, 0xCA, 0x63 + }, + { + 0x00, 0x3D, 0xAA, 0xD5, 0xE9, 0xA9, 0x99, + 0xFF, 0x1D, 0x01, 0x2C, 0xCD, 0xAC, 0x8B, + 0x28, 0x5D, 0x2B, 0xF6, 0x84, 0x9C, 0x0D, + 0xD8, 0x5D, 0x6C, 0x03, 0x2F, 0x72, 0xE0, + 0xE6, 0xAA, 0x0A, 0x2E + }, + { + 0x2B, 0x9F, 0x75, 0x2D, 0xA6, 0x67, 0x9D, + 0x1D, 0x0E, 0x5C, 0x38, 0xC1, 0x95, 0x5E, + 0xB1, 0x48, 0x69, 0x67, 0xAE, 0x8A, 0x09, + 0x39, 0x1F, 0xDC, 0xEB, 0x85, 0xD8, 0xFE, + 0xF3, 0x35, 0x29, 0xC8 + }, + { + 0x97, 0xFB, 0x9C, 0x03, 0xAE, 0xAC, 0x9E, + 0x8B, 0x63, 0xB5, 0x51, 0x47, 0x69, 0x43, + 0x80, 0x05, 0x37, 0x8E, 0x6B, 0x5D, 0x59, + 0xE1, 0xA3, 0xE4, 0x95, 0x69, 0x02, 0x73, + 0x5C, 0xCA, 0x20, 0xC6 + }, + { + 0x78, 0x64, 0xE1, 0x5F, 0x5D, 0x53, 0x9E, + 0xC0, 0x2A, 0xE1, 0x28, 0xE3, 0x89, 0x84, + 0xBF, 0xA4, 0xFF, 0x7B, 0x18, 0x52, 0xFD, + 0x22, 0x27, 0xDD, 0xAA, 0x09, 0x1C, 0x80, + 0xA5, 0x25, 0x94, 0x8A + }, + { + 0x98, 0x29, 0xE6, 0xF9, 0x35, 0xBD, 0xB2, + 0x51, 0xA5, 0xAF, 0xFF, 0x8F, 0x16, 0x01, + 0x22, 0x44, 0xCB, 0x5F, 0xD5, 0xC3, 0x23, + 0x9B, 0xC9, 0xCA, 0xFE, 0x20, 0x0B, 0x9D, + 0x06, 0xE9, 0x5E, 0x2B + }, + { + 0x3D, 0x8B, 0x79, 0xB4, 0x39, 0xB4, 0xED, + 0xED, 0x89, 0x33, 0xAF, 0xF2, 0x5F, 0x76, + 0x11, 0xC4, 0x7B, 0xBE, 0xE8, 0x40, 0x64, + 0x75, 0x54, 0x98, 0x8E, 0xF4, 0x34, 0xED, + 0x91, 0x52, 0xED, 0x51 + }, + { + 0x98, 0x4E, 0x08, 0x99, 0x2D, 0x70, 0x47, + 0xE6, 0xC9, 0x87, 0x58, 0xE6, 0x85, 0x49, + 0x99, 0x8C, 0xAA, 0x6E, 0xF3, 0x87, 0xF4, + 0x13, 0x38, 0xE2, 0x96, 0x0F, 0x19, 0xC7, + 0x8F, 0x39, 0xEB, 0x40 + }, + { + 0x1C, 0xEC, 0x7F, 0x4B, 0xA2, 0x60, 0x76, + 0x27, 0xC0, 0xA8, 0x23, 0xF6, 0x9E, 0x5E, + 0xE7, 0xD2, 0xFF, 0x28, 0xF8, 0x76, 0xA1, + 0xBF, 0x41, 0xE8, 0xD1, 0xFA, 0x2A, 0xAD, + 0x0C, 0x30, 0x38, 0x57 + }, + { + 0x8E, 0x5D, 0xFB, 0x6D, 0xDB, 0x14, 0x41, + 0x46, 0x76, 0x31, 0x67, 0xB5, 0x44, 0x10, + 0xCB, 0xD3, 0x5D, 0x6C, 0xD6, 0xD1, 0xFF, + 0x43, 0xC9, 0xD5, 0xB9, 0x18, 0xB3, 0xEE, + 0xEA, 0x1E, 0x95, 0xCE + }, + { + 0x9D, 0xA0, 0xEB, 0x5A, 0xE3, 0x86, 0xD8, + 0x95, 0x70, 0x55, 0x7C, 0x7D, 0xFB, 0xDC, + 0xB3, 0xD3, 0xDE, 0xED, 0xDA, 0xBB, 0x0C, + 0x2B, 0xDD, 0x4F, 0xC6, 0xF3, 0x22, 0xEF, + 0x6F, 0x3A, 0x92, 0xBA + }, + { + 0xBA, 0xBF, 0x92, 0x69, 0x62, 0x11, 0x41, + 0x4B, 0xD2, 0xBE, 0xF1, 0x10, 0x64, 0xD8, + 0xCB, 0x6A, 0x34, 0x8B, 0x2E, 0x5D, 0x60, + 0x95, 0xDA, 0x4C, 0xC4, 0xF5, 0x6D, 0x99, + 0x10, 0x0B, 0x88, 0xF5 + }, + { + 0x6B, 0x82, 0x2B, 0x44, 0x80, 0xE7, 0xD3, + 0x72, 0x5F, 0x1B, 0xBB, 0x91, 0xFE, 0xA7, + 0xFF, 0xA3, 0xF1, 0x2C, 0xE4, 0xF0, 0xD9, + 0x8E, 0xCF, 0x52, 0x20, 0x23, 0x05, 0xEC, + 0xBA, 0xF1, 0xD3, 0x83 + }, + { + 0x36, 0x1F, 0x1A, 0xF7, 0xA6, 0x52, 0x35, + 0x0E, 0x31, 0x36, 0xA5, 0x41, 0x0D, 0x66, + 0xCC, 0x45, 0xC5, 0x0A, 0xD0, 0x6F, 0x31, + 0x45, 0xA1, 0x48, 0xC8, 0x10, 0x96, 0xE1, + 0xB1, 0x39, 0x18, 0xA5 + }, + { + 0xE9, 0x5D, 0x89, 0xF7, 0x31, 0x86, 0xAC, + 0xA6, 0xF3, 0x4A, 0x74, 0xDE, 0x2C, 0x35, + 0x2E, 0xB7, 0x8F, 0x5A, 0x82, 0x0C, 0xDB, + 0x6A, 0xAA, 0xE4, 0xD7, 0xE0, 0x5E, 0xE6, + 0xB5, 0xE0, 0x07, 0x0B + }, + { + 0x3C, 0x31, 0x64, 0x3C, 0x17, 0x0E, 0x06, + 0x09, 0x1B, 0xEE, 0xD8, 0xCC, 0x4A, 0x2C, + 0x57, 0xEF, 0xC7, 0x96, 0xE5, 0xE1, 0xCE, + 0x52, 0x10, 0xA0, 0xD7, 0xB3, 0xC9, 0x70, + 0x92, 0x2E, 0xFC, 0x85 + }, + { + 0xCE, 0x63, 0xCC, 0xC6, 0x50, 0x13, 0x8F, + 0xCA, 0x5D, 0x6D, 0x13, 0xA8, 0xBE, 0x9B, + 0x7D, 0x49, 0x4B, 0x4C, 0x5C, 0x2D, 0x3E, + 0xC6, 0x0C, 0x9B, 0x8B, 0x55, 0x57, 0xD0, + 0xBC, 0x87, 0x29, 0xC3 + }, + { + 0x8E, 0x9E, 0x6E, 0x6C, 0xA3, 0xB5, 0xB6, + 0xBD, 0xE8, 0xCA, 0x58, 0xE7, 0x1C, 0xD7, + 0x85, 0xE3, 0x4A, 0x14, 0x34, 0x61, 0x24, + 0xBF, 0xEA, 0x3F, 0x90, 0x3B, 0x7F, 0xF2, + 0x27, 0xE5, 0xF8, 0x92 + }, + { + 0xDD, 0x8F, 0x83, 0xE9, 0xB8, 0xE7, 0x5E, + 0x2D, 0x90, 0xA7, 0xB6, 0x36, 0x95, 0x09, + 0x15, 0xB7, 0x82, 0xEC, 0x72, 0x90, 0x9A, + 0x88, 0xB9, 0xA0, 0x19, 0x58, 0xF9, 0x20, + 0x20, 0x57, 0xDE, 0x54 + }, + { + 0x10, 0x59, 0x01, 0xD4, 0xBF, 0xED, 0x32, + 0x9D, 0xB8, 0x75, 0x08, 0x60, 0x47, 0x9C, + 0x41, 0x99, 0x0C, 0xFC, 0x4D, 0x44, 0xF2, + 0x2E, 0x74, 0x87, 0xFC, 0x49, 0x5F, 0x4B, + 0xC0, 0x50, 0xBC, 0x91 + }, + { + 0xA8, 0xE7, 0xAE, 0x2E, 0x56, 0xBC, 0x39, + 0x1D, 0xE8, 0xAC, 0xA4, 0xCE, 0xC0, 0x39, + 0x9E, 0x70, 0xC7, 0x28, 0xF9, 0xD4, 0x54, + 0xE6, 0x85, 0xE3, 0x4F, 0xDE, 0x3F, 0xEE, + 0x5A, 0x2C, 0x25, 0x88 + }, + { + 0xFF, 0xDE, 0x7A, 0xBD, 0xD3, 0xCD, 0x73, + 0x8F, 0x60, 0x51, 0x1C, 0x1C, 0xF8, 0xA2, + 0x9A, 0x34, 0x97, 0xCD, 0x48, 0x64, 0xBE, + 0x8C, 0x2C, 0xD1, 0x57, 0xEA, 0x60, 0x17, + 0x98, 0x76, 0x86, 0xFC + }, + { + 0xF4, 0xCF, 0xC3, 0xE6, 0x61, 0xFE, 0xAA, + 0x7A, 0x10, 0x4D, 0x14, 0xD1, 0x52, 0xF0, + 0x45, 0x2A, 0x91, 0x63, 0xB0, 0x60, 0xEA, + 0x34, 0xE0, 0x85, 0x3F, 0xFB, 0xA2, 0x29, + 0x32, 0x61, 0xB2, 0xE8 + }, + { + 0x18, 0xAC, 0x03, 0x00, 0x37, 0xBB, 0xA2, + 0x13, 0x82, 0x73, 0x8B, 0x63, 0x47, 0xFC, + 0xEB, 0xAB, 0x11, 0x20, 0x13, 0xAE, 0x9D, + 0x60, 0xCD, 0x13, 0xD7, 0x3E, 0x76, 0xF9, + 0x20, 0x8C, 0x19, 0xA8 + }, + { + 0x4B, 0xD5, 0x0B, 0x1A, 0xD3, 0xAE, 0xB1, + 0x1D, 0x50, 0x84, 0x5E, 0xAC, 0x19, 0xEA, + 0x0F, 0xE7, 0xBE, 0xB6, 0xC6, 0xA1, 0x37, + 0xEB, 0x5B, 0x52, 0x33, 0x98, 0x01, 0xEC, + 0x49, 0xD0, 0xD1, 0x98 + }, + { + 0x0C, 0xF6, 0xE9, 0xBD, 0x81, 0xA7, 0x37, + 0x59, 0x6F, 0x39, 0xE9, 0x8F, 0x5F, 0x52, + 0xAF, 0xEA, 0x20, 0x4B, 0x02, 0xF7, 0xB5, + 0x37, 0x56, 0x30, 0x0C, 0x80, 0x61, 0x7E, + 0x81, 0xC2, 0xD8, 0xC0 + }, + { + 0xDA, 0x7A, 0x2F, 0x9D, 0x23, 0x7F, 0xEA, + 0x1C, 0x08, 0xFA, 0xB0, 0x74, 0x81, 0x91, + 0x7D, 0x4E, 0x03, 0x96, 0x4F, 0x5D, 0x84, + 0xDB, 0xE5, 0x71, 0xB2, 0xB8, 0x5E, 0xFA, + 0xEC, 0x9B, 0xBE, 0x63 + }, + { + 0x33, 0xB3, 0xE4, 0x22, 0xF4, 0x08, 0x36, + 0x21, 0x3F, 0x08, 0xD3, 0xA8, 0x48, 0x74, + 0x05, 0xED, 0x5C, 0xB4, 0x7B, 0x02, 0xE8, + 0x4C, 0xD9, 0xBA, 0x80, 0x29, 0x67, 0xAB, + 0x3E, 0x5A, 0xC0, 0xBB + }, + { + 0x0F, 0xCB, 0xF1, 0x97, 0xB6, 0xF6, 0xF0, + 0xB4, 0x71, 0x0D, 0x0C, 0xE5, 0x58, 0x58, + 0x0C, 0x7A, 0x45, 0x38, 0x2E, 0x40, 0xD6, + 0x3C, 0xD8, 0x29, 0x82, 0xEF, 0xAB, 0x42, + 0x9B, 0x53, 0x2B, 0xE5 + }, + { + 0x34, 0x29, 0x83, 0xBE, 0x97, 0xF3, 0xDD, + 0x45, 0x0D, 0xE5, 0x23, 0x5A, 0xDC, 0xE7, + 0x01, 0xF4, 0xA2, 0x15, 0x30, 0x2C, 0x11, + 0x83, 0x25, 0x8E, 0x23, 0xA8, 0xD4, 0xFC, + 0x28, 0x40, 0x55, 0xFB + }, + { + 0xEE, 0x04, 0x8E, 0x45, 0xFF, 0xEA, 0x05, + 0x07, 0x9D, 0x1A, 0xC3, 0xD5, 0x61, 0x7C, + 0x70, 0x19, 0x43, 0x3D, 0xBF, 0xB0, 0x65, + 0x4A, 0x89, 0xC2, 0x7C, 0xE3, 0xF2, 0x30, + 0xE0, 0x12, 0x66, 0x60 + }, + { + 0x40, 0x12, 0x00, 0x7A, 0x60, 0x75, 0xC1, + 0x69, 0xFD, 0x38, 0xDF, 0xB7, 0xD8, 0x26, + 0xE6, 0x11, 0xF4, 0xAA, 0xDB, 0x48, 0x99, + 0x14, 0x3C, 0x28, 0xBB, 0xD0, 0x14, 0x67, + 0x3E, 0x4F, 0xE0, 0x43 + }, + { + 0x08, 0x0A, 0x02, 0x3C, 0xC6, 0xC1, 0x87, + 0x04, 0xDE, 0xF5, 0x69, 0x49, 0xEF, 0xEA, + 0x00, 0xDA, 0xEB, 0x62, 0x70, 0x84, 0x3D, + 0x3C, 0x1B, 0xCA, 0xD2, 0x04, 0xD0, 0x74, + 0x86, 0xF8, 0x18, 0x02 + }, + { + 0x12, 0x40, 0x66, 0xA0, 0xAB, 0xB4, 0x33, + 0x85, 0xB5, 0xCF, 0x4D, 0xE3, 0xAB, 0x1E, + 0xA5, 0x54, 0x82, 0x2F, 0xCA, 0x27, 0x2E, + 0xD9, 0xB5, 0x69, 0xC3, 0x29, 0x98, 0x84, + 0xFA, 0xD6, 0x4C, 0xD9 + }, + { + 0x31, 0x02, 0x15, 0x0A, 0x99, 0xAF, 0x4B, + 0x28, 0x18, 0xE0, 0x92, 0x20, 0x65, 0xA5, + 0xE0, 0x43, 0xB1, 0x26, 0xA1, 0xC4, 0x10, + 0x04, 0x73, 0xC7, 0xB6, 0xE2, 0x1A, 0x8E, + 0x6F, 0x65, 0xD7, 0x89 + }, + { + 0x32, 0x74, 0x56, 0xFF, 0xB1, 0xC2, 0x96, + 0x97, 0x98, 0xBC, 0x70, 0xA8, 0x09, 0x92, + 0xB2, 0xAC, 0x45, 0xF4, 0x2B, 0x56, 0x02, + 0x1E, 0x52, 0xDC, 0xE9, 0xB4, 0x20, 0xB6, + 0x33, 0x22, 0xC0, 0x20 + }, + { + 0x64, 0xBA, 0x69, 0x8B, 0x5A, 0xB6, 0x56, + 0x31, 0x2E, 0xCC, 0xEB, 0x6C, 0x36, 0x1B, + 0xAB, 0x45, 0xB5, 0xEC, 0x3B, 0x65, 0x64, + 0x90, 0x41, 0xA8, 0x42, 0xA0, 0x32, 0x9C, + 0x34, 0x24, 0x7F, 0x4E + }, + { + 0x4A, 0x08, 0x24, 0x67, 0xB8, 0x96, 0xBB, + 0x69, 0xC2, 0xCD, 0x80, 0x60, 0xF3, 0x03, + 0x08, 0x05, 0x48, 0x92, 0x69, 0x7B, 0x8B, + 0xBA, 0xD9, 0x03, 0x67, 0xDA, 0x9C, 0x8A, + 0x81, 0xC2, 0x9B, 0xF4 + }, + { + 0xC0, 0x93, 0x9C, 0x2E, 0x7C, 0x44, 0xC0, + 0xB3, 0x93, 0x3D, 0xBE, 0x57, 0xCE, 0x16, + 0x99, 0x72, 0x63, 0xF6, 0xAB, 0x2D, 0x58, + 0xB9, 0x27, 0x1D, 0xE7, 0xBE, 0x11, 0xE7, + 0xED, 0x5A, 0xBC, 0x45 + }, + { + 0x24, 0x51, 0xEE, 0xF4, 0x73, 0xF6, 0x1E, + 0x33, 0x43, 0x61, 0x75, 0x4D, 0x03, 0xC0, + 0x2C, 0x57, 0x73, 0xFD, 0x70, 0x56, 0xD6, + 0x94, 0xF4, 0xA0, 0xB5, 0x9A, 0xEE, 0x59, + 0x17, 0xE1, 0x83, 0x4B + }, + { + 0x83, 0x4C, 0xD7, 0x0A, 0xED, 0xC0, 0xB6, + 0x07, 0xCC, 0x71, 0x8B, 0x3F, 0xB9, 0xC4, + 0x99, 0x27, 0xE9, 0x31, 0x04, 0x4F, 0x16, + 0x6F, 0x18, 0xAD, 0x2C, 0xFA, 0xD2, 0x30, + 0xD9, 0xE8, 0xE5, 0x3C + }, + { + 0xD0, 0xC4, 0xBD, 0x88, 0x81, 0x92, 0x22, + 0x1C, 0x56, 0xCD, 0xE0, 0x36, 0xB9, 0xFE, + 0x61, 0xFD, 0xA7, 0x02, 0x8F, 0xB1, 0x69, + 0xDF, 0x1A, 0x68, 0xB8, 0xBF, 0x89, 0xC6, + 0x6E, 0x65, 0x95, 0x50 + }, + { + 0xF8, 0x98, 0xB9, 0x65, 0xEF, 0x41, 0x55, + 0x96, 0xD8, 0x61, 0x82, 0x38, 0x33, 0xEB, + 0x5F, 0x88, 0xB9, 0x3B, 0x99, 0x3B, 0x31, + 0x40, 0x55, 0xB3, 0xCF, 0x62, 0x77, 0xB5, + 0x3B, 0x35, 0xA2, 0xEC + }, + { + 0x2D, 0xEB, 0x50, 0x66, 0xCB, 0xE8, 0x18, + 0x0D, 0x1D, 0x0D, 0x58, 0x5C, 0x64, 0xFB, + 0x35, 0x1C, 0x16, 0x63, 0xF0, 0x93, 0x56, + 0x24, 0x1A, 0xD5, 0x57, 0x81, 0x83, 0x6F, + 0xF6, 0x86, 0xC5, 0x2C + }, + { + 0x33, 0x95, 0xD6, 0xA7, 0x75, 0xA6, 0x7D, + 0x80, 0xA7, 0x1A, 0x9E, 0xEC, 0xC8, 0x0A, + 0xDB, 0x19, 0xC6, 0xCC, 0x4D, 0x6A, 0x17, + 0x25, 0x1E, 0x00, 0x19, 0x54, 0x07, 0x5C, + 0x5B, 0x96, 0x7D, 0x7C + }, + { + 0x90, 0xBE, 0xC2, 0x9F, 0x4C, 0x2E, 0xAC, + 0xB6, 0xBE, 0x42, 0x2F, 0x01, 0x74, 0xE4, + 0x55, 0x64, 0x1B, 0x50, 0x33, 0x16, 0x3D, + 0x62, 0x39, 0x55, 0xDE, 0xD7, 0x23, 0xD1, + 0x33, 0x25, 0xFE, 0x75 + }, + { + 0x47, 0x20, 0x7F, 0xA6, 0x7C, 0x4D, 0x18, + 0x29, 0x93, 0x1D, 0xBD, 0x7A, 0xBB, 0xCE, + 0x8B, 0xE1, 0x19, 0xB5, 0x19, 0xCC, 0x9D, + 0x6C, 0xC3, 0x4C, 0xDC, 0xF8, 0x06, 0xF3, + 0x7C, 0x55, 0x65, 0x59 + }, + { + 0xE9, 0x8B, 0xC5, 0x43, 0x39, 0x3C, 0x4A, + 0xA5, 0xBE, 0xB9, 0x5F, 0x8B, 0x83, 0x0D, + 0xC6, 0x5B, 0xB0, 0xC5, 0x59, 0x0D, 0xD9, + 0xE0, 0xCB, 0xF9, 0xDC, 0x6C, 0x0A, 0xD5, + 0xAB, 0xC5, 0x19, 0x01 + }, + { + 0xAF, 0x9A, 0x8B, 0xA3, 0x11, 0xC8, 0xB4, + 0xE5, 0x33, 0x62, 0xCA, 0x4D, 0xDD, 0x39, + 0xB9, 0x21, 0x67, 0x92, 0x01, 0x35, 0x81, + 0xA8, 0xED, 0x26, 0x6F, 0xE6, 0x4B, 0xB2, + 0xC2, 0x82, 0x0D, 0x9C + }, + { + 0xF7, 0xBB, 0x69, 0xDF, 0x72, 0xB9, 0x03, + 0xC6, 0x26, 0x64, 0xA5, 0x28, 0xF6, 0x48, + 0xBC, 0xF6, 0xBF, 0xBB, 0x28, 0x77, 0x25, + 0xB6, 0x45, 0xCF, 0xD2, 0x23, 0x98, 0xA6, + 0x2D, 0xBF, 0xF1, 0x30 + }, + { + 0xBA, 0xC0, 0xA6, 0x4D, 0x7D, 0xE2, 0x94, + 0x3F, 0xDB, 0x02, 0x37, 0x91, 0x4B, 0x9E, + 0x53, 0x3D, 0xA0, 0xA8, 0x3C, 0x84, 0x8F, + 0x92, 0x9A, 0x87, 0xC6, 0xCB, 0x1E, 0xA0, + 0xA1, 0x6B, 0x76, 0x79 + }, + { + 0xFA, 0xC8, 0xE7, 0x59, 0x5D, 0xD7, 0xFF, + 0x50, 0xD9, 0x11, 0x6D, 0xC3, 0x85, 0xD0, + 0x3D, 0x5D, 0x52, 0xE8, 0x6B, 0x6D, 0xE6, + 0xD4, 0x00, 0x96, 0x85, 0x70, 0x95, 0x48, + 0x07, 0x91, 0xEF, 0xAB + }, + { + 0x4C, 0x78, 0xF8, 0xF5, 0x7C, 0x5D, 0x28, + 0x3C, 0xFA, 0xAC, 0x1B, 0xE2, 0x11, 0x52, + 0x64, 0x75, 0x8A, 0xBD, 0x34, 0x4D, 0x62, + 0x6A, 0xFD, 0x5F, 0x23, 0xBA, 0xE4, 0xB5, + 0xDB, 0x3F, 0xC6, 0x4D + }, + { + 0xE9, 0xB2, 0xE8, 0x44, 0xC7, 0xF7, 0x0D, + 0x3D, 0xDF, 0xEE, 0x62, 0x6C, 0xC0, 0x03, + 0xFB, 0x22, 0x40, 0xDD, 0x88, 0xE9, 0x54, + 0x8C, 0x00, 0x25, 0x22, 0xE1, 0x37, 0x8A, + 0x0A, 0x76, 0x1D, 0x64 + }, + { + 0x74, 0x6D, 0x38, 0xD1, 0x54, 0x05, 0x64, + 0x19, 0x35, 0xFF, 0xDD, 0xA2, 0x19, 0x54, + 0x71, 0xCC, 0x2E, 0x5F, 0x94, 0xBF, 0x09, + 0x84, 0x14, 0x5E, 0xDD, 0x32, 0xD9, 0xD8, + 0xEE, 0x17, 0xB9, 0x21 + }, + { + 0x89, 0x50, 0xB2, 0x88, 0x53, 0x4C, 0xE7, + 0x5F, 0xC6, 0xB1, 0xBC, 0xF3, 0xBF, 0xB3, + 0xE3, 0xF7, 0xD0, 0x55, 0x6E, 0x5E, 0xAA, + 0x95, 0xFF, 0x8F, 0xFE, 0x03, 0xA4, 0x1A, + 0xE1, 0xF6, 0xB4, 0x0F + }, + { + 0x7A, 0x73, 0x16, 0x49, 0x46, 0x27, 0xC4, + 0x7D, 0x81, 0xA2, 0x64, 0x5D, 0x38, 0x87, + 0x2B, 0x59, 0x2D, 0xE0, 0xCB, 0x96, 0xD3, + 0x9B, 0xE6, 0x7D, 0x92, 0xF4, 0xCA, 0x44, + 0x56, 0x7F, 0xB2, 0xCC + }, + { + 0xD1, 0x1B, 0x30, 0xE5, 0x36, 0xEC, 0xD4, + 0x46, 0x3C, 0xDA, 0xEF, 0x40, 0xC9, 0xB8, + 0x25, 0x63, 0x29, 0x5E, 0x9F, 0x2B, 0x24, + 0xEF, 0xA8, 0x89, 0x8F, 0x6D, 0xB3, 0x8D, + 0xCC, 0x04, 0x78, 0x65 + }, + { + 0x80, 0x0D, 0x48, 0x89, 0xA1, 0x67, 0x96, + 0x67, 0x33, 0x22, 0x27, 0xB4, 0x7D, 0xF4, + 0x98, 0x22, 0xCA, 0xDD, 0x59, 0x13, 0x15, + 0x55, 0xE1, 0xB6, 0x9B, 0x6C, 0x7B, 0x70, + 0xC5, 0xCB, 0x90, 0x73 + }, + { + 0x4B, 0xAC, 0x09, 0x96, 0xA1, 0x79, 0x77, + 0xE9, 0x18, 0x81, 0xB2, 0xC4, 0x2F, 0xAB, + 0x8C, 0xC5, 0x65, 0xB3, 0x8F, 0x51, 0xAC, + 0x9D, 0x83, 0x35, 0x2D, 0x60, 0xA6, 0xE2, + 0x82, 0x50, 0xA7, 0x6D + }, + { + 0x20, 0xF7, 0xCF, 0xBD, 0x4C, 0xD1, 0xA1, + 0xAF, 0xED, 0x88, 0xC2, 0x73, 0xF1, 0xD4, + 0x96, 0x4A, 0x12, 0x38, 0x60, 0x75, 0x96, + 0xE6, 0x45, 0xEC, 0x8B, 0x2E, 0x73, 0x5B, + 0xB9, 0x3D, 0x3C, 0xDC + }, + { + 0x09, 0x50, 0x6D, 0x93, 0x2D, 0xD1, 0x57, + 0xF6, 0x89, 0x8E, 0x7F, 0x7C, 0x16, 0xDE, + 0xC7, 0x7D, 0x64, 0x3B, 0x53, 0xF1, 0xA2, + 0xE5, 0x84, 0x0A, 0x76, 0x75, 0x01, 0xB4, + 0x88, 0x97, 0x0E, 0xA5 + }, + { + 0xE5, 0x6E, 0x8D, 0x10, 0x44, 0xF0, 0x8F, + 0xF0, 0x76, 0x8B, 0x3F, 0xE6, 0xEB, 0x4C, + 0x18, 0xF3, 0x91, 0x1A, 0x39, 0x65, 0xA8, + 0x0B, 0xBB, 0x89, 0x1B, 0x22, 0xB1, 0x8E, + 0x6D, 0xD6, 0xBE, 0xBC + }, + { + 0xD6, 0x00, 0x35, 0xFC, 0xDC, 0xDF, 0xEF, + 0xF2, 0x04, 0x36, 0x5C, 0xD8, 0x0C, 0x1D, + 0x9A, 0xAD, 0xAA, 0x28, 0x09, 0xEF, 0x40, + 0xD3, 0xAD, 0xBA, 0xF0, 0x01, 0xDB, 0x84, + 0xC2, 0x3C, 0xFF, 0x68 + }, + { + 0xA6, 0x1A, 0x12, 0x60, 0xB0, 0x52, 0x44, + 0x77, 0x32, 0xDD, 0x49, 0x8B, 0xAB, 0x19, + 0xD1, 0x8C, 0x28, 0x57, 0x2A, 0x97, 0xC5, + 0x76, 0xAA, 0x4B, 0x8D, 0xCB, 0x5C, 0xB4, + 0x54, 0xFB, 0x00, 0xCE + }, + { + 0xC0, 0x36, 0xFA, 0x72, 0xE5, 0x0C, 0x0C, + 0x03, 0x6F, 0xFA, 0x0D, 0x7C, 0xC4, 0x37, + 0xEE, 0x97, 0xDD, 0x6F, 0x03, 0x4C, 0xB3, + 0x10, 0x5D, 0x56, 0x85, 0x42, 0x4E, 0x84, + 0x71, 0x53, 0x63, 0x7A + }, + { + 0xA5, 0xDB, 0xCB, 0x43, 0x18, 0xE6, 0x9C, + 0x2C, 0x5F, 0x0D, 0x2D, 0x04, 0x83, 0xBF, + 0x7A, 0xB9, 0x85, 0x50, 0x78, 0x68, 0xFE, + 0xE0, 0xA4, 0x68, 0x53, 0xFD, 0xC7, 0xEE, + 0x00, 0xD2, 0x90, 0xB2 + }, + { + 0xD5, 0xD0, 0xC8, 0x9C, 0xCA, 0x98, 0x7D, + 0x74, 0xA2, 0xDC, 0xDD, 0xE6, 0xB8, 0x51, + 0x54, 0x87, 0xC1, 0xAA, 0x8A, 0x91, 0x67, + 0xA9, 0xAD, 0xFB, 0x4C, 0x65, 0x10, 0x33, + 0xAE, 0x1E, 0xF6, 0x4B + }, + { + 0x0D, 0x19, 0xBF, 0x36, 0x2A, 0x3D, 0x2E, + 0x81, 0x4A, 0x6A, 0xF2, 0x67, 0xC1, 0x03, + 0xB3, 0xF3, 0x34, 0x34, 0xE5, 0x55, 0xB5, + 0xA4, 0x6D, 0x47, 0xD9, 0x36, 0x8E, 0xEB, + 0x4C, 0xFA, 0xB8, 0x1E + }, + { + 0xCF, 0x65, 0x61, 0x9E, 0x11, 0x6B, 0x99, + 0x77, 0x16, 0xD6, 0xC3, 0x50, 0x40, 0x40, + 0x13, 0xE2, 0xA2, 0x23, 0x53, 0xCF, 0x84, + 0x5A, 0xC1, 0x0E, 0xAC, 0xFB, 0x10, 0xED, + 0x13, 0x29, 0xB6, 0x91 + }, + { + 0x2C, 0x33, 0x3C, 0x8D, 0x84, 0x6A, 0x45, + 0x15, 0xC1, 0x86, 0x23, 0xDF, 0xD9, 0x1B, + 0xA8, 0xD7, 0x9A, 0x62, 0x82, 0xED, 0xCE, + 0x54, 0x50, 0xFE, 0xCB, 0x9A, 0xB4, 0x5A, + 0x06, 0x7C, 0xF0, 0xB1 + }, + { + 0xE3, 0x54, 0xD8, 0xA9, 0xFD, 0x74, 0x1E, + 0x2C, 0x3E, 0xC4, 0x20, 0x31, 0xE8, 0xD8, + 0xDD, 0xE5, 0xCF, 0x4E, 0xCA, 0x66, 0xEA, + 0x88, 0xB6, 0x92, 0xA7, 0xE0, 0xA9, 0x03, + 0x88, 0x5B, 0x46, 0x70 + }, + { + 0x06, 0x4F, 0x84, 0xCE, 0x6C, 0x81, 0x65, + 0xF4, 0x5E, 0x89, 0x92, 0xA6, 0x64, 0xFA, + 0x4B, 0xE4, 0xAC, 0xFF, 0x2B, 0x59, 0x10, + 0x12, 0x7E, 0x8A, 0x4C, 0xD6, 0x79, 0x8D, + 0xE6, 0xDC, 0x0C, 0x23 + }, + { + 0x3D, 0xBD, 0x3E, 0x7A, 0x2A, 0x5C, 0xB5, + 0xC6, 0x99, 0xC7, 0xF1, 0xA0, 0xA9, 0x7A, + 0x0A, 0xE7, 0x64, 0xE9, 0xFE, 0xA9, 0x43, + 0xD5, 0x8B, 0xA0, 0x5B, 0xEF, 0x28, 0x2D, + 0x50, 0x4D, 0x21, 0x36 + }, + { + 0xCD, 0xD4, 0x0C, 0x1A, 0x19, 0x3E, 0xA0, + 0x4C, 0x67, 0x1A, 0xF6, 0xB7, 0x19, 0xC7, + 0x67, 0x45, 0xDC, 0x4D, 0xD8, 0x8F, 0xBE, + 0x8D, 0x7D, 0x2F, 0x9F, 0x1C, 0x7A, 0x68, + 0x7C, 0xBE, 0x58, 0x09 + }, + { + 0xA3, 0xB2, 0x59, 0x0E, 0xEC, 0x70, 0xA1, + 0xE3, 0x50, 0x3C, 0xB7, 0x40, 0x79, 0x54, + 0x0A, 0xD2, 0x43, 0x7A, 0xE7, 0xF6, 0x1C, + 0xDA, 0x42, 0x3E, 0x86, 0xF5, 0x7D, 0x3E, + 0x8E, 0x28, 0x14, 0x71 + }, + { + 0xA4, 0xF0, 0x81, 0xC0, 0xFE, 0xE6, 0xAB, + 0x5C, 0x05, 0xDC, 0xA9, 0x6A, 0x82, 0xCA, + 0x58, 0x09, 0x96, 0xAC, 0x6F, 0x7A, 0x1D, + 0xB6, 0xB9, 0x5A, 0x31, 0x35, 0x6B, 0x2B, + 0x87, 0x9C, 0x27, 0xB3 + }, + { + 0x12, 0x4D, 0xD9, 0xB3, 0xDC, 0x96, 0x8C, + 0x59, 0xB5, 0xCE, 0x2F, 0x99, 0x3D, 0xFA, + 0x38, 0xE0, 0x30, 0xAB, 0x4D, 0x65, 0xDA, + 0xC2, 0xFB, 0x0D, 0xFD, 0x9A, 0x61, 0xAD, + 0x09, 0xEC, 0x50, 0x21 + }, + { + 0xF1, 0xD8, 0x97, 0xC9, 0x72, 0x67, 0x7D, + 0x95, 0x67, 0x18, 0x11, 0x7B, 0xFA, 0x06, + 0x4C, 0x09, 0x74, 0x47, 0x46, 0x7A, 0x69, + 0x63, 0x9A, 0x8B, 0x8D, 0xD5, 0x88, 0x74, + 0xB5, 0xEC, 0x4D, 0x1D + }, + { + 0x31, 0xEF, 0xC0, 0x0E, 0x84, 0x65, 0xA4, + 0x82, 0x49, 0xE6, 0xB0, 0xA2, 0xDE, 0x7E, + 0xBE, 0x92, 0x8F, 0x2E, 0x7C, 0xED, 0x39, + 0x43, 0x05, 0xC0, 0x12, 0x44, 0x61, 0x94, + 0xD9, 0x5B, 0xFC, 0x43 + }, + { + 0xA2, 0xAC, 0x37, 0xCC, 0x80, 0xD4, 0x5B, + 0xB4, 0xFB, 0xE5, 0xDA, 0x1B, 0x05, 0xE6, + 0x86, 0xAF, 0x01, 0x79, 0xAF, 0x1A, 0xC2, + 0xA9, 0x0D, 0xB9, 0x82, 0xA3, 0x02, 0x04, + 0x6B, 0x0C, 0x2A, 0xA3 + }, + { + 0xE2, 0x30, 0x6B, 0xEA, 0x41, 0x92, 0x58, + 0xDF, 0x30, 0xD9, 0x06, 0x15, 0x2F, 0x97, + 0xDE, 0x36, 0x2A, 0x65, 0x1E, 0x7A, 0xF3, + 0x20, 0xFE, 0x2B, 0xA2, 0xFC, 0xF0, 0xBB, + 0xA2, 0x9E, 0x5A, 0x08 + }, + { + 0x30, 0xE7, 0xDA, 0x1A, 0xE0, 0xF7, 0x40, + 0x2C, 0x98, 0x39, 0x53, 0x77, 0x42, 0xA3, + 0xF3, 0x43, 0xF7, 0xA6, 0x3C, 0x4C, 0xBB, + 0x20, 0xB4, 0x19, 0x56, 0x5B, 0xB5, 0x6E, + 0xB2, 0x2E, 0x3F, 0x65 + }, + { + 0xC4, 0xC8, 0xF2, 0x15, 0x57, 0x53, 0xB5, + 0x6A, 0xFC, 0xC6, 0xFA, 0x44, 0xDE, 0x1F, + 0xB3, 0x93, 0x05, 0x10, 0x64, 0x21, 0x82, + 0xDF, 0x31, 0x3F, 0x6B, 0x94, 0xF4, 0xAD, + 0x43, 0xB1, 0xB1, 0x84 + }, + { + 0x32, 0xCF, 0x7A, 0x28, 0xB4, 0x9F, 0xF7, + 0xE7, 0x95, 0xB2, 0x7F, 0x42, 0xF4, 0x18, + 0x8E, 0x2A, 0xE6, 0x30, 0xEA, 0x1B, 0xDB, + 0x48, 0xF5, 0x48, 0x4E, 0x87, 0x7D, 0x19, + 0xB9, 0x6C, 0xBC, 0x4D + }, + { + 0x75, 0x81, 0x8A, 0x7A, 0xFE, 0x73, 0x62, + 0x01, 0xE3, 0x9C, 0xCA, 0x3B, 0xAD, 0xA1, + 0x7A, 0x71, 0xFE, 0x70, 0x62, 0x23, 0x54, + 0xAB, 0x31, 0x00, 0xA3, 0xE6, 0x8D, 0xC1, + 0x7C, 0xD2, 0x0B, 0x9A + }, + { + 0x0D, 0x4B, 0x95, 0x36, 0x8B, 0x1B, 0xB5, + 0xB3, 0x65, 0x22, 0xF1, 0x71, 0xA8, 0xE1, + 0xC1, 0xD4, 0xD8, 0xA7, 0x30, 0x3E, 0xB8, + 0xD3, 0x33, 0x98, 0x70, 0x1E, 0x4D, 0xD2, + 0x43, 0xC8, 0xC9, 0xEE + }, + { + 0x7C, 0x26, 0x7B, 0x34, 0x53, 0x94, 0xA1, + 0xB6, 0xA4, 0x70, 0xE4, 0x42, 0x58, 0x9B, + 0x9E, 0xF3, 0xD9, 0xB2, 0x19, 0x24, 0xD8, + 0xC2, 0x98, 0xCB, 0x27, 0x83, 0x72, 0x78, + 0x66, 0x7F, 0x1A, 0x98 + }, + { + 0xA0, 0xCA, 0x3B, 0x8D, 0x9B, 0x2C, 0x21, + 0x11, 0x3F, 0x73, 0x3B, 0x30, 0x6D, 0xA4, + 0x78, 0x57, 0xA4, 0xF9, 0xC8, 0xBB, 0xBE, + 0xB7, 0x7F, 0x54, 0x7C, 0x19, 0x77, 0xC5, + 0x45, 0xCF, 0xD5, 0x82 + }, + { + 0x7A, 0x5B, 0xA1, 0x22, 0xEE, 0xE9, 0x78, + 0x32, 0xB0, 0xA9, 0x82, 0x9F, 0x90, 0xCC, + 0xE4, 0xE8, 0x0E, 0x7E, 0x71, 0x2E, 0x84, + 0xE1, 0x50, 0x29, 0x24, 0x7F, 0x86, 0x62, + 0xFA, 0x21, 0xA2, 0x14 + }, + { + 0xD6, 0xC7, 0xF6, 0x7E, 0xFF, 0xE2, 0xAA, + 0x30, 0x84, 0x63, 0x8C, 0x81, 0x4D, 0xFC, + 0x3F, 0x31, 0x28, 0xFA, 0xAC, 0x62, 0x90, + 0x5F, 0x8D, 0x55, 0x9B, 0x36, 0x44, 0xFD, + 0x25, 0xC8, 0x87, 0x07 + }, + { + 0xB4, 0x83, 0x5A, 0x26, 0xA7, 0x6A, 0xFC, + 0x11, 0x48, 0xC3, 0xCA, 0xB1, 0xDB, 0x1D, + 0xC1, 0x41, 0x2D, 0xA6, 0x2A, 0x5C, 0xBD, + 0x97, 0x64, 0x71, 0xA8, 0x13, 0x88, 0xDB, + 0x99, 0x6E, 0x04, 0x89 + }, + { + 0x8B, 0x60, 0x29, 0x9F, 0x69, 0xA4, 0xCC, + 0x83, 0x92, 0x1E, 0x32, 0x59, 0xB7, 0x03, + 0x98, 0x2F, 0x0A, 0x5E, 0xDD, 0x34, 0x28, + 0x0E, 0xF0, 0xD9, 0xC7, 0x98, 0x05, 0xB2, + 0x3A, 0xA9, 0xC2, 0x54 + }, + { + 0xFE, 0x62, 0xB1, 0xCF, 0x39, 0x78, 0x86, + 0xB3, 0xBF, 0x47, 0x18, 0xCF, 0xA1, 0xCA, + 0xB8, 0xF9, 0x4F, 0xB2, 0x6D, 0x5D, 0x51, + 0xC5, 0x46, 0xB3, 0x77, 0xD0, 0x20, 0x97, + 0x07, 0xB2, 0x25, 0x4C + }, + { + 0x7D, 0x8A, 0x2A, 0xEF, 0x50, 0x3B, 0x9A, + 0xCD, 0x30, 0xF1, 0x48, 0x35, 0x41, 0xB5, + 0x19, 0xC2, 0x5B, 0xDF, 0x67, 0x5E, 0x93, + 0x89, 0xBA, 0x99, 0xD3, 0x12, 0xEC, 0xFE, + 0x37, 0x88, 0x4E, 0x64 + }, + { + 0x5F, 0x9E, 0x88, 0x8B, 0x4F, 0xE2, 0xBC, + 0x06, 0xEC, 0x13, 0x2B, 0x1A, 0x3E, 0x31, + 0xC2, 0xEE, 0xC0, 0xA4, 0x40, 0x5F, 0xD3, + 0xAB, 0xA4, 0x7D, 0xBC, 0xD4, 0x61, 0xED, + 0x6D, 0xBA, 0x71, 0x85 + }, + { + 0x9F, 0xC2, 0xD4, 0x74, 0x65, 0x5A, 0xC8, + 0x59, 0x04, 0x33, 0x0A, 0x45, 0x63, 0x33, + 0x84, 0x36, 0xDF, 0x94, 0xE7, 0xE0, 0x24, + 0xF2, 0xC1, 0xFF, 0x68, 0x58, 0xD1, 0xB0, + 0x30, 0x8E, 0x31, 0x48 + }, + { + 0x73, 0x00, 0xF1, 0xED, 0x9E, 0xB6, 0x65, + 0xF2, 0x1A, 0xAB, 0xEE, 0x4D, 0x02, 0x16, + 0xE6, 0xB1, 0x01, 0x8C, 0x3C, 0xE5, 0xD8, + 0xC2, 0xFF, 0xD2, 0xF0, 0x34, 0x70, 0x2C, + 0xC0, 0xF4, 0x30, 0x4A + }, + { + 0xD8, 0x9E, 0x22, 0xDF, 0x4B, 0xE0, 0x6D, + 0x2D, 0x59, 0xFF, 0x43, 0xCC, 0x29, 0xF7, + 0xB8, 0xEC, 0x75, 0xA6, 0x18, 0x0B, 0xFD, + 0x50, 0x50, 0xF2, 0xB7, 0xE1, 0xC7, 0xFB, + 0xE2, 0xFF, 0x59, 0x1F + }, + { + 0xE0, 0xE9, 0x19, 0x3F, 0xB0, 0x26, 0xAA, + 0x2B, 0x2C, 0xA4, 0xE8, 0xBF, 0x92, 0xED, + 0x25, 0x4A, 0xB8, 0xE8, 0x4D, 0x3D, 0x44, + 0x31, 0x08, 0xD0, 0x57, 0xC7, 0x78, 0x62, + 0x3B, 0xAC, 0x88, 0x70 + }, + { + 0xDB, 0x6C, 0xA5, 0x1E, 0xD9, 0xB6, 0x51, + 0xF5, 0x5A, 0x4C, 0xB1, 0xD0, 0x24, 0x24, + 0xF1, 0x71, 0x72, 0x3C, 0x29, 0x7F, 0x7D, + 0x8A, 0x38, 0x22, 0x4C, 0xD3, 0xB5, 0x4C, + 0xA8, 0x62, 0xB0, 0x7D + }, + { + 0x5C, 0x0A, 0xA5, 0x9F, 0xE2, 0xF5, 0x5E, + 0x10, 0x63, 0xE8, 0xFE, 0x09, 0x21, 0x5B, + 0x57, 0x13, 0xD8, 0xE4, 0x15, 0xA8, 0x02, + 0x83, 0x29, 0x25, 0xB7, 0x46, 0x66, 0xA9, + 0xB5, 0xDF, 0xEF, 0x9C + }, + { + 0xCC, 0xEE, 0x10, 0x09, 0x7C, 0xEE, 0x96, + 0x38, 0x03, 0x55, 0x09, 0xA9, 0x7A, 0x62, + 0x2B, 0x1D, 0xC3, 0xBC, 0x5A, 0xAC, 0x0F, + 0xD9, 0x01, 0x16, 0xBD, 0x43, 0xB8, 0xC5, + 0xE7, 0x1F, 0x9C, 0x19 + }, + { + 0x1B, 0xFB, 0xDA, 0x11, 0x46, 0xFF, 0xD9, + 0x12, 0x8C, 0x08, 0xD1, 0x91, 0x86, 0x4C, + 0x1B, 0x5E, 0x83, 0x93, 0x87, 0x1B, 0x8B, + 0x19, 0xBA, 0x5E, 0x8E, 0x3C, 0x5C, 0xAE, + 0x0C, 0x06, 0x78, 0x9A + }, + { + 0xA2, 0x70, 0xAB, 0xDF, 0x8B, 0x0D, 0x10, + 0x3B, 0xE7, 0x68, 0xEC, 0x6A, 0xF0, 0xF9, + 0x3B, 0xBE, 0x5A, 0xC4, 0xEE, 0xB6, 0xB7, + 0x80, 0x7E, 0x99, 0x39, 0x81, 0x45, 0x7D, + 0x2C, 0x22, 0x5E, 0x12 + }, + { + 0x83, 0x55, 0x86, 0x36, 0x8F, 0x1E, 0xC1, + 0x28, 0x29, 0xA7, 0x8F, 0x51, 0xDA, 0x40, + 0x17, 0xD6, 0x87, 0xEC, 0x84, 0x29, 0xB1, + 0xBC, 0x27, 0x11, 0xBC, 0xAA, 0x27, 0xAC, + 0xB6, 0xCA, 0x38, 0x5A + }, + { + 0xB4, 0x3E, 0x11, 0xB8, 0xF1, 0x36, 0x9B, + 0x7E, 0xF0, 0xDF, 0xFF, 0x62, 0x18, 0xDB, + 0xF4, 0xB8, 0x28, 0xB2, 0xBE, 0xD0, 0x91, + 0xE8, 0x5E, 0x58, 0xEE, 0xC4, 0xCD, 0xF3, + 0x68, 0x8B, 0xA5, 0x7E + }, + { + 0x1F, 0x2E, 0x07, 0x8A, 0x24, 0x6A, 0x8A, + 0x32, 0xCB, 0xAA, 0xCE, 0x6D, 0xD8, 0xC0, + 0x10, 0xA1, 0xF2, 0x78, 0x24, 0x69, 0x4E, + 0xA1, 0xC1, 0x5D, 0xC6, 0x08, 0x71, 0x77, + 0x51, 0xCF, 0xF2, 0x49 + }, + { + 0x9E, 0xA2, 0xD9, 0xC2, 0x91, 0xA9, 0x48, + 0x08, 0xC4, 0x88, 0x74, 0xE1, 0x6E, 0x10, + 0x7B, 0xBA, 0xE7, 0x45, 0xED, 0xE1, 0xAA, + 0x67, 0xBF, 0x63, 0xE9, 0x59, 0x32, 0xEE, + 0x98, 0x4D, 0xB2, 0xC1 + }, + { + 0x09, 0x65, 0x7A, 0x30, 0x96, 0x8D, 0x33, + 0xB6, 0x6B, 0x53, 0x07, 0xFC, 0x38, 0x33, + 0x86, 0x1B, 0xD1, 0x20, 0x38, 0x9B, 0x33, + 0x3B, 0x7C, 0x96, 0xD9, 0xF4, 0x79, 0x00, + 0x6C, 0xAC, 0xDD, 0x16 + }, + { + 0x94, 0x8E, 0x9E, 0xFF, 0x27, 0x27, 0x5C, + 0x73, 0x2E, 0x82, 0x1D, 0xE6, 0x9F, 0x03, + 0x4E, 0xA1, 0x7B, 0x72, 0x45, 0x21, 0x08, + 0x64, 0x9C, 0x2B, 0x42, 0x5A, 0x51, 0x2B, + 0xB2, 0xCE, 0x90, 0xFE + }, + { + 0xB3, 0xEE, 0xA0, 0xB5, 0x63, 0x26, 0x09, + 0x58, 0xA5, 0x6C, 0xC1, 0x39, 0xF7, 0xDF, + 0x52, 0x8E, 0xE8, 0xA7, 0x9A, 0x87, 0x63, + 0xA7, 0xAB, 0x04, 0x72, 0x81, 0xBA, 0x61, + 0x5E, 0xEC, 0x92, 0xD0 + }, + { + 0xF5, 0x84, 0x03, 0xBA, 0x73, 0xC2, 0xD0, + 0x70, 0xC9, 0xC5, 0x59, 0xD3, 0xA8, 0xFA, + 0xE6, 0x64, 0x57, 0xBC, 0x0C, 0xC8, 0xB3, + 0x46, 0xC5, 0xA6, 0xA1, 0x22, 0xFB, 0x57, + 0x7F, 0xCF, 0x54, 0x24 + }, + { + 0x95, 0xE7, 0x02, 0xF8, 0xB4, 0x96, 0xF4, + 0xB6, 0x54, 0x9C, 0xEE, 0x9E, 0x6A, 0x7C, + 0xB5, 0x9E, 0xB9, 0x35, 0x5C, 0xC1, 0x76, + 0x89, 0xFF, 0x51, 0xDB, 0xAB, 0xCD, 0x0D, + 0xCF, 0x76, 0x9C, 0x37 + }, + { + 0x9D, 0xEC, 0xE8, 0xD8, 0x42, 0xDA, 0x68, + 0xA8, 0x59, 0xA4, 0xCF, 0x57, 0x41, 0x03, + 0x6B, 0x7B, 0xF7, 0xA9, 0x4F, 0xD1, 0xE9, + 0x62, 0xAE, 0xE8, 0x9E, 0x02, 0x96, 0xA9, + 0x82, 0x8D, 0xF7, 0x27 + }, + }, + { /* group_pk */ + 0x02, 0x5B, 0x7B, 0xAA, 0x65, 0x29, 0xE6, 0x84, 0x50, + 0x75, 0x38, 0xB3, 0xF0, 0x66, 0xCD, 0xA2, 0x6D, 0x29, + 0xE3, 0x29, 0x5C, 0x30, 0x65, 0xCC, 0xE3, 0xC5, 0x85, + 0x5C, 0x0D, 0x89, 0x60, 0x78, 0x1D + }, + { /* msg */ + 0x01, 0x86, 0x08, 0x2D, 0xF9, 0x58, 0x00, 0xB6, 0xEB, + 0x4E, 0x8C, 0xA2, 0x69, 0x26, 0x28, 0xAF, 0x61, 0xA4, + 0xBA, 0xED, 0x46, 0xAA, 0x89, 0xEC, 0x4D, 0xA5, 0x64, + 0xBD, 0x03, 0xA3, 0x8B, 0xE7 + }, + { /* sid */ + 0xCF, 0x72, 0x10, 0x0C, 0xDC, 0xAA, 0x4D, 0xBF, 0x8E, + 0x30, 0x9E, 0x36, 0xF0, 0x4C, 0xBF, 0x48, 0xF6, 0x37, + 0x26, 0x9A, 0xA7, 0x6D, 0x52, 0xEE, 0x95, 0xBC, 0xC4, + 0x79, 0x00, 0x68, 0xA6, 0x66 + }, + { /* cosigner_pk */ + 0x02, 0x8C, 0x03, 0x5E, 0xB9, 0x76, 0x5E, 0x3E, 0xC7, + 0xF8, 0xE1, 0x63, 0x23, 0xB7, 0x0E, 0x68, 0xBB, 0xCF, + 0x7B, 0xFB, 0x19, 0xE2, 0xFB, 0x2B, 0x6B, 0x6F, 0x70, + 0x8B, 0x52, 0xD4, 0xF3, 0xA7, 0x33 + }, + { /* cosigner_pubnonce */ + 0x02, 0x8A, 0xB3, 0x20, 0x85, 0x31, 0x24, 0xE7, 0x60, + 0x29, 0x8B, 0x22, 0x26, 0xBB, 0x7D, 0x6F, 0x6A, 0x68, + 0x20, 0x5A, 0xFC, 0xD5, 0xD3, 0xA3, 0xA5, 0xAA, 0x6D, + 0x1E, 0x8C, 0x17, 0xA8, 0x1F, 0x56, 0x03, 0x00, 0x88, + 0x1A, 0x74, 0x3A, 0xA2, 0xF4, 0x6D, 0x7B, 0x7D, 0xBA, + 0x5D, 0x7A, 0x95, 0xEE, 0x13, 0x62, 0x8D, 0x52, 0x3F, + 0xE0, 0x4F, 0xD4, 0xB4, 0xA5, 0x19, 0xC6, 0xFD, 0xCE, + 0x39, 0x86, 0x14 + }, + { /* group_aggnonce */ + 0x03, 0x6C, 0xAA, 0x6E, 0x46, 0x23, 0x06, 0x5E, 0xD6, + 0x3F, 0xF6, 0x76, 0x05, 0xA7, 0xFA, 0x0A, 0xC8, 0x81, + 0xFE, 0x70, 0x92, 0x5F, 0xA4, 0xFB, 0x9F, 0xE1, 0xEB, + 0x80, 0xE5, 0x0D, 0xEC, 0xD3, 0xB7, 0x03, 0x6C, 0x8A, + 0x33, 0x55, 0xF0, 0x79, 0x65, 0x85, 0xA6, 0x13, 0x1A, + 0xEC, 0x6A, 0x8D, 0x54, 0xF4, 0x14, 0x2D, 0x86, 0x3B, + 0x0D, 0xE1, 0x61, 0xF2, 0xA4, 0xAB, 0xCF, 0x3A, 0xFC, + 0xF3, 0x96, 0x6A + }, + { /* group_pubnonce */ + 0x03, 0x6C, 0xAA, 0x6E, 0x46, 0x23, 0x06, 0x5E, 0xD6, + 0x3F, 0xF6, 0x76, 0x05, 0xA7, 0xFA, 0x0A, 0xC8, 0x81, + 0xFE, 0x70, 0x92, 0x5F, 0xA4, 0xFB, 0x9F, 0xE1, 0xEB, + 0x80, 0xE5, 0x0D, 0xEC, 0xD3, 0xB7, 0x02, 0xC9, 0x10, + 0x88, 0x26, 0x67, 0x7C, 0xAF, 0x7D, 0xB2, 0xAE, 0xD8, + 0x3C, 0x9D, 0x11, 0xBF, 0xF7, 0x9E, 0x1A, 0xFB, 0x68, + 0xB2, 0x77, 0x64, 0x25, 0xBC, 0x79, 0x23, 0xD1, 0x9C, + 0xAF, 0x40, 0xAE + }, + { /* group_psig */ + 0x2C, 0x74, 0x3F, 0xCD, 0xAF, 0x2A, 0x5A, 0x42, 0xE1, + 0x24, 0x4C, 0xFA, 0x16, 0x4C, 0xCF, 0xF8, 0x05, 0x3F, + 0x84, 0xE1, 0x6B, 0xC0, 0xCD, 0x9F, 0x3C, 0xC8, 0x2A, + 0x4E, 0x29, 0xF0, 0x9C, 0x18 + }, + { /* pubshares, participant 1 first */ + { + 0x02, 0x10, 0x23, 0x37, 0x70, 0x46, 0x45, + 0xB9, 0x94, 0xAD, 0x8D, 0xDA, 0x61, 0x0F, + 0xD2, 0xED, 0x32, 0xC0, 0xB2, 0x63, 0xD6, + 0xB5, 0xBA, 0x91, 0x11, 0xC1, 0x66, 0xBD, + 0xF0, 0x24, 0x1B, 0x7B, 0xF3 + }, + { + 0x03, 0x77, 0x75, 0x78, 0x54, 0xEB, 0xD7, + 0xBF, 0x26, 0x66, 0xAC, 0x43, 0xFE, 0xE3, + 0xFC, 0x34, 0x1D, 0xAC, 0x78, 0x3B, 0x68, + 0xE9, 0xAA, 0xFA, 0x2F, 0xEE, 0x98, 0x6B, + 0x55, 0x7F, 0xE9, 0xAC, 0x38 + }, + { + 0x03, 0xB2, 0xCD, 0x73, 0xD1, 0x7E, 0x2C, + 0x29, 0x74, 0xCA, 0x53, 0xCF, 0x1D, 0x36, + 0x10, 0x50, 0x63, 0x14, 0xD9, 0xFE, 0x7C, + 0x49, 0xEA, 0x59, 0x22, 0xBF, 0x10, 0xBA, + 0xB1, 0x4E, 0x46, 0x02, 0xEE + }, + { + 0x03, 0x41, 0x17, 0x48, 0xD4, 0x44, 0xC0, + 0x0F, 0x83, 0x6E, 0x01, 0xFA, 0xCB, 0xF1, + 0xFB, 0x50, 0xEF, 0x15, 0x55, 0xC9, 0xE4, + 0x0A, 0x81, 0x8B, 0xCF, 0x62, 0x3E, 0x75, + 0xC4, 0xCC, 0x92, 0x3D, 0x0F + }, + { + 0x03, 0x46, 0xC7, 0xFF, 0xFC, 0xC9, 0x20, + 0x7D, 0x79, 0x1C, 0xCB, 0x6F, 0xE0, 0x2A, + 0xE0, 0xFE, 0xC0, 0xC5, 0x1D, 0x11, 0x6D, + 0x8D, 0xE1, 0xBE, 0x87, 0x86, 0xED, 0x65, + 0xF0, 0x27, 0x93, 0xAD, 0x71 + }, + { + 0x03, 0x4E, 0xBA, 0x73, 0x07, 0x79, 0xE2, + 0xE2, 0x65, 0x62, 0x2E, 0x29, 0xFF, 0xDE, + 0x5C, 0x72, 0xB1, 0xB6, 0x5F, 0x0C, 0xB1, + 0x96, 0x8E, 0x3A, 0xBA, 0x8F, 0x74, 0x2E, + 0xC5, 0x2D, 0x64, 0x5B, 0xBC + }, + { + 0x02, 0xF2, 0x5C, 0x2D, 0x3C, 0xF4, 0xDB, + 0x3B, 0x74, 0x1C, 0xD0, 0xFE, 0x48, 0xB7, + 0xBE, 0x9B, 0x90, 0x2B, 0x61, 0x54, 0x55, + 0xC6, 0x47, 0x1A, 0x8B, 0xF0, 0xF1, 0xF2, + 0x92, 0x6C, 0x09, 0x3F, 0xA6 + }, + { + 0x02, 0x53, 0x9F, 0xA6, 0x62, 0xFD, 0x83, + 0x76, 0x65, 0x7C, 0xD2, 0x08, 0x61, 0x11, + 0x4E, 0x16, 0x7C, 0xFD, 0x5A, 0xD9, 0x59, + 0x54, 0x60, 0xFF, 0x34, 0x7E, 0xA8, 0x1F, + 0xE2, 0xB1, 0x49, 0xBF, 0x76 + }, + { + 0x03, 0x66, 0x2E, 0xA6, 0x27, 0x62, 0x0B, + 0xFE, 0x2C, 0xE0, 0x1D, 0x34, 0xAC, 0x39, + 0xF0, 0xA8, 0x0D, 0x15, 0x3A, 0xCE, 0x0F, + 0x55, 0xFF, 0x8C, 0xE8, 0x4A, 0x7E, 0x18, + 0x50, 0xBD, 0xDF, 0xC1, 0x20 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x03, 0xE2, 0x17, 0xF2, 0x3D, 0x66, 0xCE, + 0xC6, 0x73, 0x56, 0x66, 0x2D, 0x1B, 0x1C, + 0x50, 0x89, 0x7A, 0xD9, 0xB6, 0x79, 0x47, + 0xEA, 0x9D, 0x37, 0x91, 0x1A, 0x1B, 0xB1, + 0xBA, 0x6B, 0x50, 0x3F, 0xDD, 0x03, 0x37, + 0x4B, 0x08, 0xC4, 0x1E, 0x7C, 0xB7, 0x96, + 0xAD, 0x68, 0x04, 0xC2, 0xD7, 0x20, 0x44, + 0x3E, 0xDF, 0x7F, 0x16, 0x8A, 0xBA, 0xE2, + 0x4F, 0xEE, 0xDC, 0x38, 0xF1, 0x00, 0x27, + 0x2A, 0xDB, 0xBF + }, + { + 0x02, 0x0A, 0x68, 0xF4, 0x2F, 0x07, 0x09, + 0x67, 0xA2, 0xB8, 0xBC, 0xEA, 0x9B, 0xF1, + 0x2B, 0xC3, 0xB3, 0xFC, 0xE4, 0x50, 0x8F, + 0x07, 0x5E, 0xDE, 0x8D, 0xE2, 0x94, 0x01, + 0xDD, 0xB9, 0xEE, 0x3F, 0x13, 0x03, 0xFF, + 0x28, 0xEB, 0xA4, 0xF0, 0x63, 0x79, 0xA0, + 0x89, 0xDE, 0x5C, 0xB2, 0x96, 0xEC, 0x98, + 0x9B, 0x45, 0x0B, 0x24, 0x33, 0x79, 0x0F, + 0xC6, 0x12, 0xDC, 0xBB, 0xD6, 0x62, 0x89, + 0x72, 0xA0, 0xA1 + }, + { + 0x03, 0x07, 0x61, 0x4D, 0x80, 0xF1, 0x60, + 0x90, 0xCF, 0x99, 0xDA, 0x0D, 0x02, 0x7A, + 0x16, 0x53, 0xDE, 0xB5, 0x37, 0x3B, 0xF1, + 0x73, 0x0B, 0x87, 0xA6, 0xFB, 0xEB, 0x46, + 0xA3, 0x47, 0x64, 0xDB, 0xFD, 0x03, 0xBE, + 0x62, 0x9B, 0xBD, 0xE8, 0x37, 0x0F, 0xAF, + 0x54, 0x6A, 0x96, 0x31, 0x7A, 0x94, 0x68, + 0xA6, 0x21, 0x07, 0xF3, 0x76, 0xC7, 0xAC, + 0x15, 0x42, 0xD3, 0x59, 0x83, 0x1C, 0x2A, + 0xB9, 0x89, 0x9F + }, + { + 0x02, 0x51, 0x95, 0x06, 0xC1, 0x85, 0x8E, + 0x45, 0x91, 0x94, 0x34, 0xDA, 0xEB, 0xEB, + 0x04, 0xC8, 0xB2, 0x7B, 0xB3, 0x3A, 0x0A, + 0x63, 0x9B, 0x68, 0xA5, 0x44, 0x5E, 0xD9, + 0x96, 0xC6, 0xAA, 0x1E, 0x17, 0x03, 0x14, + 0xE0, 0x82, 0x8E, 0xA7, 0x69, 0x90, 0xAC, + 0x21, 0x60, 0x1B, 0xA9, 0xEA, 0x1E, 0x47, + 0xEA, 0x7F, 0x43, 0x63, 0xF2, 0x7F, 0x85, + 0x69, 0x3E, 0x3C, 0x96, 0xB9, 0xB6, 0xB3, + 0xB6, 0x2F, 0x84 + }, + { + 0x03, 0x4F, 0xCF, 0xFE, 0x39, 0xC2, 0x83, + 0x05, 0xEE, 0xD0, 0xBC, 0x16, 0xD2, 0xBF, + 0x5F, 0x62, 0x9E, 0xE0, 0x34, 0xDD, 0xCC, + 0x2E, 0xEB, 0xCC, 0xD1, 0xD4, 0xDA, 0xBF, + 0x3D, 0x30, 0x12, 0x50, 0x68, 0x02, 0x87, + 0xFB, 0x9A, 0xE3, 0xB4, 0x48, 0x9C, 0x1F, + 0xC9, 0xC8, 0x01, 0xE0, 0xA3, 0x1E, 0x31, + 0x53, 0xC3, 0xAF, 0x94, 0x5A, 0xEE, 0x7E, + 0x3C, 0x79, 0x4E, 0xEF, 0x14, 0x50, 0x15, + 0xC4, 0x41, 0xDE + }, + { + 0x03, 0x83, 0x80, 0xC4, 0x79, 0x82, 0x6B, + 0x2F, 0xC1, 0x0C, 0x5E, 0x4D, 0xDF, 0x2F, + 0xD6, 0x30, 0xD5, 0x47, 0x8D, 0x5D, 0xA7, + 0x76, 0x4D, 0x4D, 0x3F, 0x83, 0xA9, 0x7B, + 0x2E, 0xF3, 0x8C, 0xF7, 0xED, 0x02, 0xBB, + 0xED, 0x86, 0x10, 0x43, 0xBE, 0xF3, 0x0D, + 0xA2, 0xD9, 0x88, 0x38, 0x1B, 0xDC, 0xC6, + 0x24, 0x51, 0x2C, 0xE4, 0x70, 0x58, 0x15, + 0xEA, 0xDE, 0xCF, 0xE4, 0xF8, 0xC9, 0x1C, + 0x4A, 0x78, 0x50 + }, + { + 0x02, 0x1B, 0xCA, 0xDA, 0x51, 0x97, 0xD5, + 0x27, 0x11, 0x2A, 0x85, 0xAB, 0xFD, 0xF7, + 0xDF, 0xA0, 0x14, 0x73, 0x8E, 0x06, 0xAE, + 0x71, 0x5A, 0x7C, 0x4C, 0x92, 0x93, 0x65, + 0x89, 0xEA, 0xC8, 0xE3, 0x44, 0x03, 0x89, + 0xED, 0xBE, 0x93, 0xFD, 0xF2, 0x56, 0xEA, + 0x89, 0xF8, 0x66, 0x0A, 0xBA, 0x93, 0xA2, + 0x25, 0x6B, 0xB7, 0x6A, 0xF5, 0x97, 0xB1, + 0xC8, 0x51, 0x7B, 0x4F, 0x3B, 0x07, 0x9F, + 0x23, 0xB8, 0xC5 + }, + { + 0x03, 0xA3, 0x85, 0xAE, 0x22, 0xF5, 0x7B, + 0xDA, 0x6F, 0x88, 0x98, 0x30, 0xED, 0x1B, + 0x54, 0x48, 0xEB, 0xB5, 0x76, 0x7D, 0x8B, + 0x92, 0xBE, 0x40, 0xE6, 0x4C, 0xE1, 0xE8, + 0x11, 0xCD, 0x66, 0xC9, 0x67, 0x03, 0x63, + 0x2A, 0xF2, 0x91, 0x4C, 0x48, 0x18, 0x0D, + 0x3D, 0xF0, 0xDC, 0x6D, 0xD4, 0x05, 0x4E, + 0x63, 0x65, 0xB9, 0x72, 0x0E, 0xBC, 0x55, + 0x1C, 0x3A, 0xF5, 0xE4, 0xEA, 0xA5, 0x1D, + 0xB4, 0x0E, 0x6B + }, + { + 0x02, 0x6B, 0xEE, 0x8E, 0xA3, 0xD9, 0xA5, + 0x8F, 0xBB, 0x22, 0x65, 0xF2, 0x07, 0x12, + 0x7B, 0x5B, 0x8D, 0x35, 0xB2, 0x3D, 0x94, + 0x45, 0xB1, 0x26, 0x12, 0x28, 0xA0, 0xB8, + 0x49, 0x1A, 0xB3, 0x20, 0x57, 0x02, 0x57, + 0xEA, 0xF4, 0x0B, 0x4C, 0x78, 0x2A, 0x1F, + 0x38, 0x93, 0x46, 0x9B, 0xAA, 0x04, 0xED, + 0x72, 0x29, 0x5A, 0xEE, 0xC3, 0x2F, 0x87, + 0xB7, 0x5E, 0xEC, 0x2E, 0x90, 0x5D, 0x63, + 0x6A, 0x87, 0x22 + }, + }, + { /* signature shares, the first t participants */ + { + 0xB3, 0x26, 0x3B, 0x72, 0x86, 0x74, 0xA7, + 0x5F, 0x7D, 0x63, 0xA3, 0x20, 0x22, 0x87, + 0xEE, 0xC4, 0xB4, 0xF2, 0xD7, 0xAD, 0x51, + 0x8D, 0xD0, 0x4B, 0xF4, 0x60, 0x31, 0x6A, + 0x5D, 0xA3, 0x18, 0xC5 + }, + { + 0x4C, 0x56, 0xFC, 0xAD, 0xD9, 0x45, 0xC0, + 0x39, 0xE3, 0x53, 0x50, 0x2D, 0xB5, 0x49, + 0x14, 0x92, 0x2D, 0x97, 0x20, 0x1F, 0xDC, + 0x60, 0x64, 0xB0, 0x52, 0xAA, 0xA7, 0x08, + 0xE2, 0x3F, 0xF7, 0x72 + }, + { + 0x83, 0xDE, 0x75, 0x87, 0x1A, 0xFC, 0xB5, + 0x94, 0xF0, 0x78, 0xE6, 0xFA, 0x3E, 0x8F, + 0x11, 0x3A, 0xEF, 0x2C, 0x27, 0x6D, 0x46, + 0x30, 0xBD, 0x6C, 0xC9, 0xFD, 0xFF, 0xF5, + 0x4F, 0xB9, 0x09, 0xF9 + }, + { + 0xEF, 0xE0, 0x50, 0x2B, 0x59, 0xA2, 0xD2, + 0xE9, 0xB7, 0xA1, 0x31, 0xA4, 0xCE, 0xF9, + 0x5A, 0x71, 0xFB, 0x36, 0x46, 0x10, 0x9F, + 0x28, 0xFC, 0xC0, 0xFC, 0x58, 0xE1, 0xFD, + 0xCB, 0x9E, 0x52, 0xF7 + }, + { + 0x30, 0xCB, 0xEE, 0xED, 0x3D, 0xEB, 0x9E, + 0x67, 0x80, 0xE0, 0x31, 0x94, 0x17, 0xC8, + 0x0B, 0xC1, 0x8F, 0x6D, 0x40, 0xB2, 0x7C, + 0xED, 0xD4, 0xC7, 0x7B, 0x34, 0x82, 0x7F, + 0xD9, 0x54, 0x47, 0x0D + }, + }, + }, +}; + diff --git a/src/modules/iceberg/vpss.h b/src/modules/iceberg/vpss.h new file mode 100644 index 00000000..bac34f57 --- /dev/null +++ b/src/modules/iceberg/vpss.h @@ -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 */ diff --git a/src/modules/iceberg/vpss_impl.h b/src/modules/iceberg/vpss_impl.h new file mode 100644 index 00000000..1e3c87c3 --- /dev/null +++ b/src/modules/iceberg/vpss_impl.h @@ -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 */ diff --git a/src/secp256k1.c b/src/secp256k1.c index 427c491d..d1b2f7c6 100644 --- a/src/secp256k1.c +++ b/src/secp256k1.c @@ -960,3 +960,7 @@ static int secp256k1_ge_parse_ext(secp256k1_ge* ge, const unsigned char *in33) { #ifdef ENABLE_MODULE_CHILLDKG # include "modules/chilldkg/main_impl.h" #endif + +#ifdef ENABLE_MODULE_ICEBERG +# include "modules/iceberg/main_impl.h" +#endif diff --git a/src/tests.c b/src/tests.c index c221cb1c..112c6f8d 100644 --- a/src/tests.c +++ b/src/tests.c @@ -7928,6 +7928,10 @@ static void run_ecdsa_wycheproof(void) { # include "modules/chilldkg/tests_impl.h" #endif +#ifdef ENABLE_MODULE_ICEBERG +# include "modules/iceberg/tests_impl.h" +#endif + static void run_secp256k1_memczero_test(void) { unsigned char buf1[6] = {1, 2, 3, 4, 5, 6}; unsigned char buf2[sizeof(buf1)]; @@ -8301,6 +8305,9 @@ static const struct tf_test_module registry_modules[] = { #endif #ifdef ENABLE_MODULE_CHILLDKG MAKE_TEST_MODULE(chilldkg), +#endif +#ifdef ENABLE_MODULE_ICEBERG + MAKE_TEST_MODULE(iceberg), #endif MAKE_TEST_MODULE(utils), }; From 072f6631f3d25d2024801760d0b17ad13f5c4b59 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 31 Aug 2026 12:25:09 +0200 Subject: [PATCH 2/6] iceberg: add the example program Port examples/iceberg.c from the source tree: a full Iceberg session demonstrating the call order from the module docs -- distributed key generation, pubshare_gen/pubkey_agg to obtain the group public key, nonce_gen/nonce_agg into an ordinary MuSig2 public nonce, and partial_sign/partial_sig_agg into an ordinary MuSig2 partial signature. One content adaptation: the secp256k1_musig_nonce_process call gains a NULL adaptor argument, matching this repo's zkp musig variant. Wired like the chilldkg example: autotools noinst_PROGRAMS + TESTS entry under ENABLE_MODULE_ICEBERG (the example runs as part of make check), CMake example target in examples/CMakeLists.txt, and iceberg_example added to .gitignore. Verified: ./iceberg_example runs to completion (exit 0) under both build systems. --- .gitignore | 1 + Makefile.am | 11 + examples/CMakeLists.txt | 4 + examples/iceberg.c | 725 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 741 insertions(+) create mode 100644 examples/iceberg.c diff --git a/.gitignore b/.gitignore index cd3d8fd4..c762c8e6 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ contrib/gh-pr-create.sh frost_example chilldkg_example +iceberg_example ### CMake /CMakeUserPresets.json diff --git a/Makefile.am b/Makefile.am index d07b3abb..056ff369 100644 --- a/Makefile.am +++ b/Makefile.am @@ -234,6 +234,17 @@ chilldkg_example_LDFLAGS += -lbcrypt endif TESTS += chilldkg_example endif +if ENABLE_MODULE_ICEBERG +noinst_PROGRAMS += iceberg_example +iceberg_example_SOURCES = examples/iceberg.c +iceberg_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC +iceberg_example_LDADD = libsecp256k1.la +iceberg_example_LDFLAGS = -static +if BUILD_WINDOWS +iceberg_example_LDFLAGS += -lbcrypt +endif +TESTS += iceberg_example +endif endif ### Precomputed tables diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 50795899..d04e6645 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -39,3 +39,7 @@ endif() if(SECP256K1_ENABLE_MODULE_CHILLDKG) add_example(chilldkg) endif() + +if(SECP256K1_ENABLE_MODULE_ICEBERG) + add_example(iceberg) +endif() diff --git a/examples/iceberg.c b/examples/iceberg.c new file mode 100644 index 00000000..9e1ec935 --- /dev/null +++ b/examples/iceberg.c @@ -0,0 +1,725 @@ +/************************************************************************* + * To the extent possible under law, the author(s) have dedicated all * + * copyright and related and neighboring rights to the software in this * + * file to the public domain worldwide. This software is distributed * + * without any warranty. For the CC0 Public Domain Dedication, see * + * EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 * + *************************************************************************/ + +/** A 3-of-7 group signing beside an ordinary MuSig2 cosigner. + * + * The group behaves as one MuSig2 participant, and the finished signature is + * an ordinary BIP-340 signature that records nothing about the group. + * + * Three roles appear below and they run different code, which is the thing + * worth keeping straight while reading: + * + * participant holds a share, produces a nonce and a signature share, and + * never sees the whole key. There are seven of them. + * coordinator moves messages around and combines them. Untrusted: every + * check here assumes it is hostile. + * cosigner an ordinary MuSig2 signer that knows nothing about any of + * this and calls the plain musig API. + * + * The participants are wiped between the two rounds and rebuilt from storage, + * because no secret nonce survives that gap; see reboot_participants for what + * does survive it, which is not nothing. One call is made that is expected to + * fail, in refusals_are_refused. + * + * See also include/secp256k1_iceberg.h and doc/iceberg.md. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +/* The trusted dealer lives in its own header, and is not part of the installed + * API. See the note at the top of it. */ +#include + +#include "examples_util.h" + +#define N 7 /* participants in the group */ +#define T 3 /* how many of them can sign */ +#define MU (2 * T - 1) /* how many must take part in round one */ + +/* One participant's memory: one secret, and one note to itself. + * + * The share is the secret and never changes. `answered` is the highest session + * label this participant has signed under. The library does not know about it and + * could not, since it holds nothing between calls, but answering twice under one + * label gives away the key share, so somebody has to remember, and the somebody + * is the caller. */ +struct participant { + secp256k1_iceberg_share share; + unsigned char answered[32]; +}; + +/* The same participant's disk. A reboot loses the struct above, not this one, + * and both fields have to come back. See reboot_participants. */ +struct storage { + unsigned char share_bytes[SECP256K1_ICEBERG_SHARE_MAX_LEN]; + size_t share_len; + unsigned char answered_bytes[32]; +}; + +/* The threshold side. Set up once, then unchanged for the life of the group. */ +struct group { + struct participant member[N]; + struct storage disk[N]; + secp256k1_iceberg_pubshare pubshare[N]; + secp256k1_pubkey pubkey; +}; + +/* The other side: an ordinary MuSig2 signer, which knows nothing about any of + * the above. Everything it holds is its own, including the secret nonce that + * the group deliberately does not have an equivalent of. */ +struct cosigner { + secp256k1_keypair keypair; + secp256k1_pubkey pubkey; + secp256k1_musig_secnonce secnonce; +}; + +/* What the two of them add up to. Computed once, at key aggregation, and the + * only thing a verifier ever sees. */ +struct shared_key { + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_xonly_pubkey output; +}; + +/* One attempt at one signature. Everything here is public. */ +struct signing_session { + unsigned char sid[32]; + secp256k1_iceberg_pubnonce contribution[MU]; + const secp256k1_iceberg_pubnonce *contribution_ptr[MU]; + secp256k1_musig_pubnonce group_nonce; /* the group's, after interpolating */ + secp256k1_musig_pubnonce cosigner_nonce; /* the cosigner's, as published */ + secp256k1_musig_aggnonce cosigner_aggnonce; /* the cosigners' alone */ + secp256k1_musig_session musig_session; +}; + +static void heading(const char *text) { + printf("\n%s\n", text); +} + +static void step(const char *text) { + printf(" %-55s", text); + fflush(stdout); +} + +/* Deal the shares and publish the group's key. + * + * This is a trusted dealer: for the length of one call, this machine holds + * enough to reconstruct the group's private key. Acceptable for testing, and + * where one party is trusted already. A distributed key generation produces + * the same shares without that moment ever existing, and this module does not + * provide one. */ +static int deal_shares(const secp256k1_context *ctx, struct group *group) { + secp256k1_iceberg_share *share_ptr[N]; + const secp256k1_iceberg_pubshare *pubshare_ptr[N]; + unsigned char seed[32]; + unsigned int k; + int ok; + + step("Dealing shares (secp256k1_iceberg_shares_gen)"); + if (!fill_random(seed, sizeof(seed))) { + return 0; + } + for (k = 0; k < N; k++) { + share_ptr[k] = &group->member[k].share; + } + ok = secp256k1_iceberg_shares_gen(ctx, share_ptr, N, T, seed); + secure_erase(seed, sizeof(seed)); + if (!ok) { + return 0; + } + printf("ok\n"); + + /* A share is one seed per (t-1)-subset that leaves this participant out, + * so it is large and grows quickly with the group. Each participant keeps + * its own and nothing else. */ + step("Storing them (secp256k1_iceberg_share_serialize)"); + for (k = 0; k < N; k++) { + group->disk[k].share_len = sizeof(group->disk[k].share_bytes); + if (!secp256k1_iceberg_share_serialize(ctx, group->disk[k].share_bytes, + &group->disk[k].share_len, &group->member[k].share)) { + return 0; + } + /* Nothing answered yet, so the lowest possible label. Do this once, at + * dealing; doing it again later throws the protection away. */ + memset(group->member[k].answered, 0, sizeof(group->member[k].answered)); + memcpy(group->disk[k].answered_bytes, group->member[k].answered, 32); + } + printf("ok, %lu bytes each\n", (unsigned long)group->disk[0].share_len); + + step("Group public key (secp256k1_iceberg_pubkey_agg)"); + for (k = 0; k < N; k++) { + if (!secp256k1_iceberg_pubshare_gen(ctx, &group->pubshare[k], &group->member[k].share, NULL)) { + return 0; + } + pubshare_ptr[k] = &group->pubshare[k]; + } + /* 2t-1 public shares are more than the key needs, since t of them already + * determine it, and the surplus is the point: they have to agree, so a + * participant that published a wrong one is caught now rather than at + * signing time. */ + if (!secp256k1_iceberg_pubkey_agg(ctx, &group->pubkey, pubshare_ptr, MU, N, T)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Aggregate the group's key with the cosigner's, and tweak the result. + * + * Nothing below this point knows that one of the two keys is a group. The + * tweak belongs to this outer session: musig_nonce_process sets its term aside + * and musig_partial_sig_agg adds it once, at the top. No Iceberg call is even + * told the tweak happened, which is what stops it being counted twice. */ +static int aggregate_keys(const secp256k1_context *ctx, struct group *group, struct cosigner *cosigner, struct shared_key *shared) { + const secp256k1_pubkey *pubkeys[2]; + secp256k1_pubkey output_pk; + unsigned char taptweak[32]; + unsigned char seckey[32]; + int ok; + + step("Cosigner keypair (secp256k1_keypair_create)"); + ok = fill_random(seckey, sizeof(seckey)) + && secp256k1_keypair_create(ctx, &cosigner->keypair, seckey) + && secp256k1_keypair_pub(ctx, &cosigner->pubkey, &cosigner->keypair); + /* The keypair holds everything needed from here on, so the raw key does not + * outlive this call; secp256k1_keypair_sec brings it back when required. */ + secure_erase(seckey, sizeof(seckey)); + if (!ok) { + return 0; + } + printf("ok\n"); + + step("Aggregating (secp256k1_musig_pubkey_agg)"); + pubkeys[0] = &group->pubkey; + pubkeys[1] = &cosigner->pubkey; + if (!secp256k1_musig_pubkey_agg(ctx, NULL, &shared->keyagg_cache, pubkeys, 2)) { + return 0; + } + printf("ok\n"); + + /* The cache records the hash of the key list, not the list, so nothing ties + * it to the group's key later. Run this once, here, rather than trusting a + * cache round two cannot check. */ + step("Checking the cache (secp256k1_iceberg_keyagg_check)"); + if (!secp256k1_iceberg_keyagg_check(ctx, &shared->keyagg_cache, pubkeys, 2, + &group->pubkey)) { + return 0; + } + printf("ok\n"); + + step("Tweaking (secp256k1_musig_pubkey_xonly_tweak_add)"); + if (!fill_random(taptweak, sizeof(taptweak)) + || !secp256k1_musig_pubkey_xonly_tweak_add(ctx, &output_pk, &shared->keyagg_cache, taptweak) + || !secp256k1_xonly_pubkey_from_pubkey(ctx, &shared->output, NULL, &output_pk)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Round one, the group's half. Note the arguments: a share and a label. No + * message, no cosigner, nothing from anybody else. */ +static int group_round_one(const secp256k1_context *ctx, struct group *group, + struct signing_session *session) { + unsigned int k; + + /* The label is the most delicate value in the scheme, and it is the + * caller's to choose. Every participant's secret nonces are a function of + * (its seeds, this label), and the seeds never change, so whoever picks the + * label picks everyone's secrets. + * + * Here it is a fixed byte string, which is fine for an example and wrong + * for anything else. In Lightning it is the commitment number: unique, + * strictly increasing over the channel's life, and known before the + * transaction is assembled, which is what lets this round run at all, + * since the message does not exist yet. */ + step("Choosing the session label"); + memset(session->sid, 0x2c, sizeof(session->sid)); + printf("ok, "); + print_hex(session->sid, 4); + + step("Participant nonces (secp256k1_iceberg_nonce_gen)"); + for (k = 0; k < MU; k++) { + if (!secp256k1_iceberg_nonce_gen(ctx, &session->contribution[k], &group->member[k].share, NULL, session->sid)) { + return 0; + } + session->contribution_ptr[k] = &session->contribution[k]; + } + printf("ok, %d of the %d participants\n", MU, N); + + /* Verification, not addition. The contributions are points on a degree + * t-1 polynomial in the exponent; this checks that they lie on one and + * then interpolates. It is most of what the group costs, and it is what + * stops a single participant biasing the group's nonce. + * + * NULL is the group's internal aggregate, which nothing takes back. */ + step("Combining them (secp256k1_iceberg_nonce_agg)"); + if (!secp256k1_iceberg_nonce_agg(ctx, &session->group_nonce, NULL, + session->contribution_ptr, MU, N, T, &group->pubkey)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Round one, the cosigner's half. Ordinary MuSig2, and it does not know a group + * exists. Runs before, after or alongside the function above. */ +static int cosigner_round_one(const secp256k1_context *ctx, struct cosigner *cosigner, struct shared_key *shared, + struct signing_session *session, const unsigned char *msg32) { + const secp256k1_musig_pubnonce *just_the_cosigner[1]; + unsigned char secrand[32]; + unsigned char seckey[32]; + int ok; + + step("Cosigner nonce (secp256k1_musig_nonce_gen)"); + ok = fill_random(secrand, sizeof(secrand)) + && secp256k1_keypair_sec(ctx, seckey, &cosigner->keypair) + && secp256k1_musig_nonce_gen(ctx, &cosigner->secnonce, &session->cosigner_nonce, + secrand, seckey, &cosigner->pubkey, + msg32, &shared->keyagg_cache, NULL); + secure_erase(secrand, sizeof(secrand)); + secure_erase(seckey, sizeof(seckey)); + if (!ok) { + return 0; + } + + /* Round two needs the cosigners' aggregate on its own, separately from the + * one that includes the group. With one cosigner it is an aggregate of one, + * which is not a special case anywhere. */ + just_the_cosigner[0] = &session->cosigner_nonce; + if (!secp256k1_musig_nonce_agg(ctx, &session->cosigner_aggnonce, just_the_cosigner, 1)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Both halves have published. Combine them into the session everybody signs + * against, which is where the message finally enters. */ +static int open_session(const secp256k1_context *ctx, struct shared_key *shared, + struct signing_session *session, const unsigned char *msg32) { + const secp256k1_musig_pubnonce *both[2]; + secp256k1_musig_aggnonce full_aggnonce; + + step("Session (secp256k1_musig_nonce_process)"); + both[0] = &session->group_nonce; + both[1] = &session->cosigner_nonce; + if (!secp256k1_musig_nonce_agg(ctx, &full_aggnonce, both, 2) + || !secp256k1_musig_nonce_process(ctx, &session->musig_session, &full_aggnonce, + msg32, &shared->keyagg_cache, NULL)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Forget the secret nonces, then rebuild from disk. + * + * A FROST signer has to keep a secret nonce alive across this line, and losing + * it, restoring an old backup over it, or running two copies of the signer are + * each catastrophic. An Iceberg participant keeps no secret nonce at all: its + * nonces are a function of its share and the label, both of which it is given + * again. + * + * That property is narrower than "keeps no state". + * A participant still has to remember which labels it has already answered + * under, because two answers under one label are two equations in its three + * secrets and three are enough to recover its key share. That bookkeeping is + * not secret, and it is not optional; it is simply not this library's, since + * only the group as a whole can decide which message a label belongs to. + * + * So wipe the secrets and prove that much. Skip the parse below and the next + * call is handed a wiped share, whose magic fails an ARG_CHECK, so it aborts + * through the illegal callback rather than returning 0. */ +static int reboot_participants(const secp256k1_context *ctx, struct group *group) { + unsigned int k; + + step("Wiping every participant's secret material"); + for (k = 0; k < N; k++) { + secure_erase(&group->member[k], sizeof(group->member[k])); + } + printf("ok\n"); + + step("Rebuilding (secp256k1_iceberg_share_parse)"); + for (k = 0; k < N; k++) { + if (!secp256k1_iceberg_share_parse(ctx, &group->member[k].share, + group->disk[k].share_bytes, + group->disk[k].share_len)) { + return 0; + } + memcpy(group->member[k].answered, group->disk[k].answered_bytes, 32); + } + printf("ok\n"); + + /* Note what came back besides the share: `answered`. That record is not + * secret, which is why it sits on the same disk in the clear, and it is + * not optional, which is why it is restored here rather than left at zero. + * A participant that forgets it will answer twice. */ + return 1; +} + +/* One label, one answer: the half of the rule a participant can enforce alone. + * + * The library cannot do this for you. It holds nothing between calls, so "have I + * answered under this label before?" is a question only the caller's storage can + * answer. Here that storage is one 32-byte field per participant and the rule is + * that a label must be strictly greater than the last one signed under, which is + * free when the label is a counter. It refuses a repeat outright rather than + * asking whether the repeat was harmless: a label is one signing attempt, so an + * honest retry arrives under a new label and never needs the exception. + * + * The record reaches disk here, before the caller has a share to publish. A + * crash between signing and storing is the same as never having stored, and the + * next boot answers the label again. + * + * The other half of the rule, that no two participants answer one label on + * different messages, cannot be checked here or anywhere else inside a + * participant, because it is a fact about what other people were shown. See + * doc/iceberg.md. */ +static int may_sign_under(struct participant *member, struct storage *disk, const unsigned char *sid32) { + if (memcmp(sid32, member->answered, 32) <= 0) { + return 0; + } + memcpy(member->answered, sid32, 32); + memcpy(disk->answered_bytes, member->answered, 32); + return 1; +} + +/* The group's half of round two, signed by participants first, first+1 and + * first+2. + * + * Taking `first` is not generality for its own sake. The point of the scheme is + * that any t of them will do, so main runs this twice with different people and + * compares the two signatures. + * + * Any t of the seven, and not only the five who were in round one. The last + * section of main proves that by having a participant who sat round one out + * sign anyway. What each signer does need is the whole round-one set to check + * against, which is a different count from the number of signers. */ +static int group_signs(const secp256k1_context *ctx, struct group *group, + struct shared_key *shared, struct signing_session *session, + unsigned int first, const unsigned char *msg32, + secp256k1_musig_partial_sig *group_partial_sig) { + secp256k1_iceberg_partial_sig sig_share[T]; + const secp256k1_iceberg_partial_sig *sig_share_ptr[T]; + unsigned int k; + + /* Note what this takes: the group's own round-one contributions, not an + * aggregate of them, and not the cosigners', which arrive separately and + * already aggregated. Do not read that as an optimization waiting to happen. + * The nesting coefficient is a hash of the group's aggregate nonce, so a + * coordinator free to invent that aggregate has a coefficient it can vary at + * will, and three invented aggregates under one label give three equations in + * the same three unknowns, and the third is the key. So the aggregate is + * derived here from the contributions instead. */ + step("Signature shares (secp256k1_iceberg_partial_sign)"); + for (k = 0; k < T; k++) { + if (!may_sign_under(&group->member[first + k], &group->disk[first + k], session->sid)) { + printf("refused: participant %d has already answered under this label\n", + first + k + 1); + return 0; + } + /* The signer derives the aggregate from the contributions, then checks + * the polynomial they determine against the contribution it derives for + * itself, which ties the set to this label. It need not have been one of + * the contributors. */ + if (!secp256k1_iceberg_partial_sign(ctx, &sig_share[k], &group->member[first + k].share, NULL, + session->sid, session->contribution_ptr, MU, + &group->pubkey, &shared->keyagg_cache, + msg32, &session->cosigner_aggnonce)) { + return 0; + } + sig_share_ptr[k] = &sig_share[k]; + } + printf("ok, from participants %d, %d and %d\n", first + 1, first + 2, first + 3); + + /* Optional, and the coordinator's to decide on. Skipping it costs nothing + * until a share is bad, at which point the final signature simply fails + * BIP-340 and says nothing about which of the three caused it. Checking + * costs about what producing a share costs, per share. + * + * That is with exactly t shares, which is what this collects. Hand + * secp256k1_iceberg_partial_sig_agg one more than it needs and it refuses a + * set that disagrees with itself, cheaply and without naming anybody, for + * the same reason pubkey_agg can above: past the threshold there is a spare + * point to check against. + * + * It answers "is this share bad", not "who is lying". A share that fails + * here may have been written by somebody other than the member it names, + * and the same 0 comes back if this machine has the wrong message. */ + step("Checking them (secp256k1_iceberg_partial_sig_verify)"); + for (k = 0; k < T; k++) { + if (!secp256k1_iceberg_partial_sig_verify(ctx, &sig_share[k], &group->pubshare[first + k], + session->contribution_ptr, MU, N, T, + &group->pubkey, &shared->keyagg_cache, msg32, + &session->cosigner_aggnonce)) { + printf("share from participant %d does not check out\n", first + k + 1); + return 0; + } + } + printf("ok, all %d\n", T); + + step("Combining (secp256k1_iceberg_partial_sig_agg)"); + if (!secp256k1_iceberg_partial_sig_agg(ctx, group_partial_sig, sig_share_ptr, T, N, T)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* The cosigner's half, which happens exactly once. + * + * secp256k1_musig_partial_sign consumes the secret nonce, zeroing it on + * the way out, precisely so that signing twice with it is not something a + * caller can do by accident. Which quorum the group fielded is not the + * cosigner's business and does not change its answer. */ +static int cosigner_signs(const secp256k1_context *ctx, struct cosigner *cosigner, + struct shared_key *shared, struct signing_session *session, + secp256k1_musig_partial_sig *cosigner_partial_sig) { + step("Cosigner's share (secp256k1_musig_partial_sign)"); + if (!secp256k1_musig_partial_sign(ctx, cosigner_partial_sig, &cosigner->secnonce, + &cosigner->keypair, &shared->keyagg_cache, + &session->musig_session)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* One group share plus one cosigner share makes an ordinary MuSig2 signature. */ +static int combine(const secp256k1_context *ctx, struct signing_session *session, + const secp256k1_musig_partial_sig *group_partial_sig, + const secp256k1_musig_partial_sig *cosigner_partial_sig, + unsigned char *sig64) { + const secp256k1_musig_partial_sig *both[2]; + + step("Final signature (secp256k1_musig_partial_sig_agg)"); + both[0] = group_partial_sig; + both[1] = cosigner_partial_sig; + if (!secp256k1_musig_partial_sig_agg(ctx, sig64, &session->musig_session, both, 2)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* One call that must be refused and one that must not, with the reason beside + * each. + * + * The refusal below is a load-bearing security check that looks like a bug from + * outside; the call after it is the case that looks refusable and must not + * be. */ +static int refusals_are_refused(const secp256k1_context *ctx, struct group *group, + struct shared_key *shared, struct signing_session *session, + const unsigned char *msg32) { + secp256k1_iceberg_partial_sig sig_share; + + step("Signing over contributions from another session"); + { + /* The contributions below are a real sharing, internally consistent + * and passing the degree check, but they were produced under a + * different label. Accepting them would give whoever supplied them a + * coefficient in the signing equation that this participant cannot + * check, which is worth a signature share to an adversary and nothing + * to anybody else. + * + * The signer catches it by interpolating the contributions and + * evaluating the result at its own index: the polynomial does not pass + * through the contribution it derives locally for the label it is + * signing under. */ + secp256k1_iceberg_pubnonce elsewhere[MU]; + const secp256k1_iceberg_pubnonce *elsewhere_ptrs[MU]; + unsigned char other_sid[32]; + unsigned int k; + + memcpy(other_sid, session->sid, sizeof(other_sid)); + other_sid[0] ^= 1; + for (k = 0; k < MU; k++) { + if (!secp256k1_iceberg_nonce_gen(ctx, &elsewhere[k], &group->member[k].share, + NULL, other_sid)) { + return 0; + } + elsewhere_ptrs[k] = &elsewhere[k]; + } + /* The set is a perfectly good sharing. What it is not is a sharing of + * *this* label, and only the signer can tell, because only the signer + * holds the share that says what its own contribution should have + * been. */ + if (secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[0].share, NULL, + session->sid, elsewhere_ptrs, MU, &group->pubkey, + &shared->keyagg_cache, msg32, + &session->cosigner_aggnonce)) { + printf("FAILED: it signed, and it should not have\n"); + return 0; + } + } + printf("refused, correctly\n"); + + /* The other half of that check is a thing it must NOT cost. */ + step("Signing by a member who sat round one out"); + { + /* Participants 6 and 7 were not among the 2t-1 who produced nonces. + * They can still sign, because a contribution is a function of the + * share and the label, so an absent member can derive what its + * contribution would have been and check the polynomial against it + * without ever having been one of the contributors. + * + * This is the property that lets a quorum change between the rounds, + * which is most of why the scheme works this way: keys sit in + * cold storage and members are routinely absent rather than hostile. */ + if (!secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[N - 1].share, NULL, + session->sid, session->contribution_ptr, MU, + &group->pubkey, &shared->keyagg_cache, msg32, + &session->cosigner_aggnonce)) { + printf("FAILED: it refused, and it should not have\n"); + return 0; + } + } + printf("signed, correctly\n"); + return 1; +} + +static void print_caveats(void) { + heading("WHAT THIS EXAMPLE IS NOT"); + printf(" A trusted dealer deals the shares. Real deployments want a\n"); + printf(" distributed key generation, which this module does not provide.\n"); + printf("\n"); + printf(" Everything is passed as structs in one process. The wire formats\n"); + printf(" exist: 34 bytes for a public share, 67 for a nonce contribution,\n"); + printf(" 33 for a signature share. Nothing here uses them, so the\n"); + printf(" network is the part you still have to write.\n"); + printf("\n"); + printf(" Structs in one process also satisfy constraint 2 for free. Over a\n"); + printf(" network the channel must be authenticated and a contribution taken\n"); + printf(" only from the member its index names; without that, an adversary\n"); + printf(" supplying t of the 2t-1 picks the group's nonce and every check\n"); + printf(" here still passes.\n"); + printf("\n"); + printf(" Verifying a share tells you whether it satisfies the equation,\n"); + printf(" not who is at fault: partial signatures are forgeable, so a share\n"); + printf(" that fails may have been written by somebody else.\n"); + printf("\n"); + printf(" may_sign_under above is half the rule, and the easy half: it\n"); + printf(" stops one participant answering twice. Nothing anywhere stops two\n"); + printf(" participants answering one label on different messages. Two answers\n"); + printf(" under one label are two equations in a participant's three\n"); + printf(" secrets; three recover its key share.\n"); + printf("\n"); + printf(" The label is an argument, rather than something derived from the\n"); + printf(" message, because round one has to run before the message exists.\n"); + printf(" In Lightning the nonce is fixed a round-trip before the\n"); + printf(" transaction is assembled, and the commitment number is what\n"); + printf(" Lightning supplies instead.\n"); + printf("\n"); + printf(" So half of that discipline is yours, never answering twice under\n"); + printf(" one label, and half is the group's: agree which message a label\n"); + printf(" belongs to before anyone answers. The unforgeability proof, which\n"); + printf(" is not yet peer-reviewed, assumes both. See doc/iceberg.md.\n"); +} + +int main(void) { + secp256k1_context *ctx; + struct group group; + struct cosigner cosigner; + struct shared_key shared; + struct signing_session session; + unsigned char msg[32] = "this_could_be_the_hash_of_a_msg"; + secp256k1_musig_partial_sig group_partial_sig, cosigner_partial_sig; + unsigned char sig[64], sig_from_the_others[64]; + unsigned int k; + int ok; + + ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + printf("Iceberg: a %d-of-%d group signing as one MuSig2 participant.\n\n", T, N); + printf(" round one needs %d of the %d participants; round two needs %d\n", MU, N, MU); + printf(" online again, not necessarily the same ones, and combines %d\n", T); + printf(" signature shares. See doc/iceberg.md for why those differ.\n"); + + heading("Setup: once, by a dealer who is then not needed again"); + ok = deal_shares(ctx, &group); + + if (ok) { + heading("Key aggregation: the group is now just a public key"); + ok = aggregate_keys(ctx, &group, &cosigner, &shared); + } + if (ok) { + /* The two halves are written in this order because something has to + * go first on the page. Swap the two calls and the example still + * passes, since neither needs anything the other produces, which is why + * two Iceberg groups can sign with each other. */ + heading("Round one: needs 2t-1 participants, in no particular order"); + ok = group_round_one(ctx, &group, &session) + && cosigner_round_one(ctx, &cosigner, &shared, &session, msg) + && open_session(ctx, &shared, &session, msg); + } + if (ok) { + heading("The gap: every participant forgets its secret nonces"); + ok = reboot_participants(ctx, &group); + } + if (ok) { + heading("Round two: t shares, from any members, not just round one's"); + ok = group_signs(ctx, &group, &shared, &session, 0, msg, &group_partial_sig) + && cosigner_signs(ctx, &cosigner, &shared, &session, &cosigner_partial_sig) + && combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig); + } + if (ok) { + step("Verifying (secp256k1_schnorrsig_verify)"); + ok = secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &shared.output); + if (ok) { + printf("ok\n "); + print_hex(sig, sizeof(sig)); + printf(" An ordinary BIP-340 signature. Nothing in it records a group.\n"); + } + } + if (ok) { + heading("The same signature, from different people"); + /* Participants 4, 5 and 6 this time, with no overlap with 1, 2 and 3, and + * 6 was not in round one either. Both of those matter. Disjoint, + * because nobody may answer twice under one label. The library holds + * nothing between calls and cannot detect that, so may_sign_under above + * is what refuses it. And 6 absent from round one, because a member that + * was away can still work out what its contribution would have been. + * + * Only the group signs again. The cosigner's share is reused, since it + * has one secret nonce and spending it twice would be nonce reuse. */ + ok = group_signs(ctx, &group, &shared, &session, T, msg, &group_partial_sig) + && combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig_from_the_others); + } + if (ok) { + step("Comparing the two signatures"); + ok = memcmp(sig, sig_from_the_others, sizeof(sig)) == 0; + printf(ok ? "byte-identical\n" : "the two quorums disagreed\n"); + } + if (ok) { + heading("What the module checks, and what it leaves to you"); + ok = refusals_are_refused(ctx, &group, &shared, &session, msg); + } + if (!ok) { + printf("FAILED\n"); + } + print_caveats(); + + /* Clear the secrets: a bug elsewhere that leaks memory, or an OS that swaps + * it to disk, should not find them lying around. The group's shares are the + * long-term secret here, and the cosigner's keypair and secret nonce are + * the rest. */ + for (k = 0; k < N; k++) { + secure_erase(&group.member[k], sizeof(group.member[k])); + secure_erase(&group.disk[k], sizeof(group.disk[k])); + } + secure_erase(&cosigner, sizeof(cosigner)); + secp256k1_context_destroy(ctx); + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} From da13028c0da7d915d06b9ad7472333d7df7fef60 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 31 Aug 2026 12:25:09 +0200 Subject: [PATCH 3/6] iceberg: add the bench_iceberg benchmark binary Port src/bench_iceberg.c, the standalone benchmark binary the module's bench_impl.h is written for (this repo's bench harness has no per-module include pattern for it, so the source tree's own wiring is mirrored instead): noinst_PROGRAMS under USE_BENCHMARK + ENABLE_MODULE_ICEBERG in Makefile.am, a bench_iceberg target in src/CMakeLists.txt, and a .gitignore entry. The benchmark covers the group configurations 2-of-3, 3-of-5, 4-of-7, 5-of-9 and 5-of-10. Verified: ./bench_iceberg builds and runs under both build systems. --- .gitignore | 1 + Makefile.am | 6 +++ src/bench_iceberg.c | 97 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/bench_iceberg.c diff --git a/.gitignore b/.gitignore index c762c8e6..62334853 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ bench_generator bench_rangeproof bench_internal bench_whitelist +bench_iceberg noverify_tests tests exhaustive_tests diff --git a/Makefile.am b/Makefile.am index 056ff369..4deb521b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -118,6 +118,12 @@ bench_internal_CPPFLAGS = $(SECP_CONFIG_DEFINES) bench_ecmult_SOURCES = src/bench_ecmult.c bench_ecmult_LDADD = $(COMMON_LIB) $(PRECOMPUTED_LIB) bench_ecmult_CPPFLAGS = $(SECP_CONFIG_DEFINES) +if ENABLE_MODULE_ICEBERG +noinst_PROGRAMS += bench_iceberg +bench_iceberg_SOURCES = src/bench_iceberg.c +bench_iceberg_LDADD = $(COMMON_LIB) $(PRECOMPUTED_LIB) +bench_iceberg_CPPFLAGS = $(SECP_CONFIG_DEFINES) +endif endif TESTS = diff --git a/src/bench_iceberg.c b/src/bench_iceberg.c new file mode 100644 index 00000000..24828109 --- /dev/null +++ b/src/bench_iceberg.c @@ -0,0 +1,97 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#include +#include +#include + +#include "secp256k1.c" +#include "../include/secp256k1.h" +#include "util.h" +#include "bench.h" +#include "modules/iceberg/bench_impl.h" + +/* Small sizes, spread out enough to show the growth curve. These are not + * deployment recommendations: all five are below the n >= 3t-2 that the + * agreement around the scheme needs, and 4-of-7 versus 5-of-9 is here because + * the seed count roughly triples between them, not because anyone runs it. */ +static const unsigned int CONFIGS[][2] = { {3,2}, {5,3}, {7,4}, {9,5}, {10,5} }; + +int main(int argc, char **argv) { + bench_iceberg_data data; + int iters = get_iters(1000); + size_t c; + + (void)argc; (void)argv; + data.ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + printf("%-22s", ""); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + char label[16]; + sprintf(label, "%u-of-%u", CONFIGS[c][1], CONFIGS[c][0]); + printf("%12s", label); + } + printf("\n"); + printf("%-22s", "seeds per participant"); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + printf("%12u", secp256k1_rss_binom(CONFIGS[c][0] - 1, CONFIGS[c][1] - 1)); + } + printf("\n\n"); + + { + /* per_sig marks the rows a signing session actually pays for, so the + * total at the bottom comes from the same run as the rows above it + * rather than from someone adding them up by hand. */ + struct { const char *name; void (*fn)(void*, int); int per_sig; const char *note; } benches[] = { + { "shares_gen", bench_iceberg_shares_gen, 0, " (setup only)" }, + { "share_cache_create",bench_iceberg_cache_create, 0, " (setup only)" }, + { "pubshare_gen", bench_iceberg_pubshare_gen, 0, " (setup only)" }, + { "pubkey_agg", bench_iceberg_pubkey_agg, 0, " (setup only)" }, + { "nonce_gen", bench_iceberg_nonce_gen, 1, "" }, + { "nonce_agg", bench_iceberg_nonce_agg, 1, "" }, + { "partial_sign", bench_iceberg_partial_sign, 1, "" }, + { "partial_sig_agg", bench_iceberg_partial_sig_agg, 1, "" }, + { "partial_sig_verify",bench_iceberg_partial_sig_verify, 0, " (optional, per share)" }, + { " degree_check", bench_iceberg_degree_check, 0, " (inside nonce_agg, partial_sign and partial_sig_verify)" }, + { " interpolate", bench_iceberg_interpolate, 0, " (inside nonce_agg, partial_sign and partial_sig_verify)" }, + { " lagrange_basis", bench_iceberg_lagrange_basis, 0, " (inside degree_check)" } + }; + double per_signature[sizeof(CONFIGS)/sizeof(CONFIGS[0])] = { 0 }; + size_t b; + for (b = 0; b < sizeof(benches)/sizeof(benches[0]); b++) { + printf("%-22s", benches[b].name); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + int64_t begin, total; + double each; + int i; + data.n = CONFIGS[c][0]; + data.t = CONFIGS[c][1]; + data.mu = 2 * data.t - 1; + bench_iceberg_setup(&data); + benches[b].fn(&data, 2); /* warm up */ + begin = gettime_i64(); + for (i = 0; i < iters; i++) { + benches[b].fn(&data, 1); + } + total = gettime_i64() - begin; + each = (double)total / iters; + if (benches[b].per_sig) { + per_signature[c] += each; + } + printf("%9.1f us", each); + } + printf("%s\n", benches[b].note); + } + + printf("\n%-22s", "per signature"); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + printf("%9.1f us", per_signature[c]); + } + printf("\n"); + } + + secp256k1_context_destroy(data.ctx); + return EXIT_SUCCESS; +} From 8c4bf548ed0c5e284c48fe5a6c702b874d5b5f6a Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 31 Aug 2026 12:25:55 +0200 Subject: [PATCH 4/6] iceberg: cover the module in ctime_tests Port the source tree's own iceberg ctime_tests block (guarded by ENABLE_MODULE_ICEBERG, after the chilldkg block): a 3-of-5 group run through shares_gen, a share serialize/parse roundtrip, share_cache_create (with a CHECKMEM_CHECK proving the cache holds no secrets), pubshare_gen, pubkey_agg, nonce_gen, nonce_agg and partial_sign, with share secrets undefined and all protocol outputs defined, following the frost/chilldkg annotation style. Both memory checkers pass with zero new declassifies needed -- the module's constant-time layering (vpss variable-time but secret-free, scalar_poly keeping secrets away from inversions) was already in place. Verified: valgrind ./ctime_tests exits 0, and the clang MemorySanitizer build exits 0 (with the chilldkg block unaffected). --- src/ctime_tests.c | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/ctime_tests.c b/src/ctime_tests.c index a47b0ffa..9635ee77 100644 --- a/src/ctime_tests.c +++ b/src/ctime_tests.c @@ -61,6 +61,11 @@ #include "../include/secp256k1_chilldkg.h" #endif +#ifdef ENABLE_MODULE_ICEBERG +#include "../include/secp256k1_iceberg.h" +#include "../include/secp256k1_iceberg_dealer.h" +#endif + static void run_tests(secp256k1_context *ctx, unsigned char *key); int main(void) { @@ -587,6 +592,119 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) { SECP256K1_CHECKMEM_DEFINE(ack_sig, sizeof(ack_sig)); } #endif + +#ifdef ENABLE_MODULE_ICEBERG + { + /* A 3-of-5 group, dealt from `key` and taken as far as one signature + * share. Secret here is the dealer's root seed and everything the + * module derives from it: the per-subset seeds, the key share, and the + * two nonce shares. Not secret: participant indices, the group and + * threshold, every Lagrange weight, the commitments, the session label, + * and both nonce coefficients. */ + enum { ICEBERG_N = 5, ICEBERG_T = 3, ICEBERG_MU = 2 * ICEBERG_T - 1 }; + secp256k1_iceberg_share shares[ICEBERG_N]; + secp256k1_iceberg_share *share_ptr[ICEBERG_N]; + secp256k1_iceberg_share_cache share_cache; + secp256k1_iceberg_pubshare pubshares[ICEBERG_N]; + const secp256k1_iceberg_pubshare *pubshare_ptr[ICEBERG_N]; + secp256k1_iceberg_pubnonce nonces[ICEBERG_N]; + const secp256k1_iceberg_pubnonce *nonce_ptr[ICEBERG_N]; + secp256k1_iceberg_aggnonce iceberg_aggnonce; + secp256k1_iceberg_partial_sig iceberg_psig; + secp256k1_musig_pubnonce group_pubnonce, cosigner_pubnonce; + const secp256k1_musig_pubnonce *cosigner_ptr[1]; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_keyagg_cache iceberg_cache; + secp256k1_pubkey group_pk, cosigner_pk; + const secp256k1_pubkey *iceberg_pk_ptr[2]; + unsigned char share_bytes[SECP256K1_ICEBERG_SHARE_MAX_LEN]; + unsigned char sid[32], cosigner_secrand[32]; + size_t share_len; + int party; + + for (party = 0; party < ICEBERG_N; party++) { + share_ptr[party] = &shares[party]; + pubshare_ptr[party] = &pubshares[party]; + nonce_ptr[party] = &nonces[party]; + } + SECP256K1_CHECKMEM_DEFINE(key, 32); + /* The cosigner needs randomness distinct from the dealer's root seed; + * any perturbation of `key` will do. */ + memcpy(cosigner_secrand, key, sizeof(cosigner_secrand)); + cosigner_secrand[0] = cosigner_secrand[0] + 3; + CHECK(secp256k1_keypair_create(ctx, &keypair, key)); + CHECK(secp256k1_keypair_pub(ctx, &cosigner_pk, &keypair)); + + SECP256K1_CHECKMEM_UNDEFINE(key, 32); + ret = secp256k1_iceberg_shares_gen(ctx, share_ptr, ICEBERG_N, ICEBERG_T, key); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + + /* Storing and restoring a share moves seed material through a buffer, + * which is where a length or an offset computed from it would show. */ + share_len = sizeof(share_bytes); + ret = secp256k1_iceberg_share_serialize(ctx, share_bytes, &share_len, &shares[0]); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + ret = secp256k1_iceberg_share_parse(ctx, &shares[0], share_bytes, share_len); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + + ret = secp256k1_iceberg_share_cache_create(ctx, &share_cache, &shares[0]); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + /* The header says the cache holds no secret, and this is the line that + * makes that a result rather than a claim. Every byte of it must be + * defined: the weights come from the group size, the threshold and the + * participant index, all of which are public, and none of them from a + * seed. Declassifying is not the same test: it would say the value may be + * published, where this says nothing secret reached it. */ + SECP256K1_CHECKMEM_CHECK(&share_cache, sizeof(share_cache)); + + for (party = 0; party < ICEBERG_N; party++) { + ret = secp256k1_iceberg_pubshare_gen(ctx, &pubshares[party], &shares[party], + party == 0 ? &share_cache : NULL); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } + + /* A commitment is public, and so is everything built from one. */ + SECP256K1_CHECKMEM_DEFINE(pubshares, sizeof(pubshares)); + CHECK(secp256k1_iceberg_pubkey_agg(ctx, &group_pk, pubshare_ptr, ICEBERG_MU, ICEBERG_N, ICEBERG_T) == 1); + + iceberg_pk_ptr[0] = &group_pk; + iceberg_pk_ptr[1] = &cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(ctx, NULL, &iceberg_cache, iceberg_pk_ptr, 2)); + SECP256K1_CHECKMEM_DEFINE(msg, sizeof(msg)); + SECP256K1_CHECKMEM_UNDEFINE(cosigner_secrand, sizeof(cosigner_secrand)); + ret = secp256k1_musig_nonce_gen(ctx, &cosigner_secnonce, &cosigner_pubnonce, + cosigner_secrand, NULL, &cosigner_pk, msg, + &iceberg_cache, NULL); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + SECP256K1_CHECKMEM_DEFINE(&cosigner_pubnonce, sizeof(cosigner_pubnonce)); + cosigner_ptr[0] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(ctx, &cosigner_aggnonce, cosigner_ptr, 1)); + memset(sid, 0x7e, sizeof(sid)); /* public: the label is the caller's to choose */ + + for (party = 0; party < ICEBERG_MU; party++) { + ret = secp256k1_iceberg_nonce_gen(ctx, &nonces[party], &shares[party], + party == 0 ? &share_cache : NULL, sid); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } + SECP256K1_CHECKMEM_DEFINE(nonces, sizeof(nonces)); + CHECK(secp256k1_iceberg_nonce_agg(ctx, &group_pubnonce, &iceberg_aggnonce, + nonce_ptr, ICEBERG_MU, ICEBERG_N, ICEBERG_T, &group_pk) == 1); + + ret = secp256k1_iceberg_partial_sign(ctx, &iceberg_psig, &shares[0], &share_cache, + sid, nonce_ptr, ICEBERG_MU, &group_pk, + &iceberg_cache, msg, &cosigner_aggnonce); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } +#endif } #if defined(__GNUC__) From 2473c76871add0d0cce76e06903d29421580a6fc Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 31 Aug 2026 12:25:55 +0200 Subject: [PATCH 5/6] iceberg: wire the module into CI - ci/ci.sh: new ICEBERG environment variable, printed in the reproduction header and passed to configure as --enable-module-iceberg (mirroring CHILLDKG); bench_iceberg runs in the bench step when the module is enabled, as in the source tree's CI. - .github/workflows/ci.yml: ICEBERG: 'no' default; ICEBERG: 'yes' in the 20 jobs that enable musig + schnorrsig + experimental (the same jobs that build chilldkg). Jobs that deliberately build without musig or its dependencies are left at 'no'. Verified programmatically: the YAML parses and every ICEBERG: 'yes' context also has MUSIG, SCHNORRSIG and EXPERIMENTAL set to 'yes'. --- .github/workflows/ci.yml | 31 +++++++++++++++++++++---------- ci/ci.sh | 7 ++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f7527aa..f00c38b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ env: SCHNORRSIG_HALFAGG: 'no' FROST: 'no' CHILLDKG: 'no' + ICEBERG: 'no' ### test options SECP256K1_TEST_ITERS: 64 BENCH: 'yes' @@ -106,14 +107,14 @@ jobs: matrix: configuration: - env_vars: { WIDEMUL: 'int64', RECOVERY: 'yes' } - - env_vars: { WIDEMUL: 'int64', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes'} + - env_vars: { WIDEMUL: 'int64', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes'} - env_vars: { WIDEMUL: 'int128' } - env_vars: { WIDEMUL: 'int128_struct', ELLSWIFT: 'yes' } - env_vars: { WIDEMUL: 'int128', RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } - - env_vars: { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes'} + - env_vars: { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes'} - env_vars: { WIDEMUL: 'int128', ASM: 'x86_64', ELLSWIFT: 'yes' } - env_vars: { RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes'} - - env_vars: { CTIMETESTS: 'no', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CPPFLAGS: '-DVERIFY' } + - env_vars: { CTIMETESTS: 'no', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CPPFLAGS: '-DVERIFY' } - env_vars: { BUILD: 'distcheck', WITH_VALGRIND: 'no', CTIMETESTS: 'no', BENCH: 'no' } - env_vars: { CPPFLAGS: '-DDETERMINISTIC' } - env_vars: { CFLAGS: '-O0', CTIMETESTS: 'no' } @@ -181,6 +182,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CC: ${{ matrix.cc }} steps: @@ -220,6 +222,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' steps: @@ -261,6 +264,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' steps: @@ -293,6 +297,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' CC: ${{ matrix.cc }} @@ -344,6 +349,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' steps: @@ -400,6 +406,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' SECP256K1_TEST_ITERS: 2 @@ -440,6 +447,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' CFLAGS: '-fsanitize=undefined,address -g' UBSAN_OPTIONS: 'print_stacktrace=1:halt_on_error=1' @@ -497,6 +505,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CC: ${{ matrix.cc }} SECP256K1_TEST_ITERS: 32 ASM: 'no' @@ -533,6 +542,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' strategy: @@ -565,15 +575,15 @@ jobs: fail-fast: false matrix: env_vars: - - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes' } + - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes' } - { WIDEMUL: 'int128_struct', ECMULTGENKB: 2, ECMULTWINDOW: 4 } - - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes' } + - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes' } - { WIDEMUL: 'int128', RECOVERY: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CC: 'gcc' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CPPFLAGS: '-DVERIFY', CTIMETESTS: 'no' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CC: 'gcc' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CPPFLAGS: '-DVERIFY', CTIMETESTS: 'no' } - BUILD: 'distcheck' steps: @@ -751,6 +761,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' steps: - *CHECKOUT diff --git a/ci/ci.sh b/ci/ci.sh index 5dbb9604..e3bab1a6 100755 --- a/ci/ci.sh +++ b/ci/ci.sh @@ -15,7 +15,7 @@ print_environment() { ECMULTWINDOW ECMULTGENKB ASM WIDEMUL WITH_VALGRIND EXTRAFLAGS \ EXPERIMENTAL ECDH RECOVERY EXTRAKEYS SCHNORRSIG MUSIG SCHNORRSIG_HALFAGG ELLSWIFT \ ECDSA_S2C GENERATOR RANGEPROOF SURJECTIONPROOF WHITELIST ECDSAADAPTOR BPPP \ - FROST CHILLDKG SECP256K1_TEST_ITERS BENCH SECP256K1_BENCH_ITERS CTIMETESTS SYMBOL_CHECK \ + FROST CHILLDKG ICEBERG SECP256K1_TEST_ITERS BENCH SECP256K1_BENCH_ITERS CTIMETESTS SYMBOL_CHECK \ EXAMPLES \ HOST WRAPPER_CMD \ CC CFLAGS CPPFLAGS AR NM \ @@ -71,6 +71,7 @@ fi --enable-module-schnorrsig-halfagg="$SCHNORRSIG_HALFAGG" \ --enable-module-frost="$FROST" \ --enable-module-chilldkg="$CHILLDKG" \ + --enable-module-iceberg="$ICEBERG" \ --enable-examples="$EXAMPLES" \ --enable-ctime-tests="$CTIMETESTS" \ --with-valgrind="$WITH_VALGRIND" \ @@ -134,6 +135,10 @@ then then $EXEC ./bench_bppp fi + if [ "$ICEBERG" = "yes" ] + then + $EXEC ./bench_iceberg + fi } >> bench.log 2>&1 fi From a68cae31caf2e8cf6742fafcbf1f8ba330d738d5 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 31 Aug 2026 12:25:55 +0200 Subject: [PATCH 6/6] iceberg: list the module in the README feature list Add the Iceberg module to the "Added features" section, after the ChillDKG entry, linking doc/iceberg.md. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4e211304..68ebc17b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Added features: * Experimental module for Schnorr signature half-aggregation. * Experimental module for [FROST (BIP 445)](src/modules/frost/frost.md). * Experimental module for [ChillDKG](src/modules/chilldkg/chilldkg.md), distributed key generation for FROST (bip-frost-dkg draft). +* Experimental module for [Iceberg](doc/iceberg.md), a threshold scheme that lets a group of parties stand in for a single MuSig2 (BIP 327) participant. Experimental features are made available for testing and review by the community. The APIs of these features should not be considered stable.