Merge branch 'iceberg'
Some checks failed
CI / Build arm64 Docker image (push) Has been cancelled
CI / Build x64 Docker image (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CC:gcc CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HA… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCH… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:y… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTION… (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[BUILD:distcheck]) (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[ECMULTGENKB:2 ECMULTWINDOW:4 WIDEMUL:int128_struct]) (push) Has been cancelled
CI / x86_64: macOS Sequoia, Valgrind (map[RECOVERY:yes WIDEMUL:int128]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes CC:gcc ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIST:yes WID… (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes CPPFLAGS:-DVERIFY ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITEL… (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIST:yes WIDEMUL:in… (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BPPP:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIST:yes WIDEMUL:int128]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[BUILD:distcheck]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[ECMULTGENKB:2 ECMULTWINDOW:4 WIDEMUL:int128_struct]) (push) Has been cancelled
CI / ARM64: macOS Sonoma (map[RECOVERY:yes WIDEMUL:int128]) (push) Has been cancelled
CI / x86 (MSVC): Windows (VS 2022) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, static) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, shared) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, int128_struct with __(u)mulh) (push) Has been cancelled
CI / x64 (MSVC): Windows (VS 2022, int128_struct) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, static) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, shared) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, int128_struct with __(u)mulh) (push) Has been cancelled
CI / x64 (clang-cl): Windows (VS 2022, int128_struct) (push) Has been cancelled
CI / x64 (MSVC): C++ (public headers) (push) Has been cancelled
CI / SageMath prover (push) Has been cancelled
CI / release (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:ye… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALF… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJE… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITEL… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RE… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNOR… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:y… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes … (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAG… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECT… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes WHITELIS… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ASM:x86_64 ELLSWIFT:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BENCH:no BUILD:distcheck CTIMETESTS:no WITH_VALGRIND:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes CPPFLAGS:-DVERIFY CTIMETESTS:no ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes RECO… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes ELLSWIFT:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRS… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes CHILLDKG:yes ECDH:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes ICEBERG:yes MUSIG:yes RANGEPROOF:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:ye… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[BPPP:yes ECDSAADAPTOR:yes ECDSA_S2C:yes EXPERIMENTAL:yes EXTRAKEYS:yes FROST:yes GENERATOR:yes MUSIG:yes RANGEPROOF:yes RECOVERY:yes SCHNORRSIG:yes SCHNORRSIG_HALFAGG:yes SURJECTIONPROOF:yes… (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[CFLAGS:-O0 CTIMETESTS:no]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[CFLAGS:-O1 ECDH:yes ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[CPPFLAGS:-DDETERMINISTIC]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ECMULTGENKB:86 ECMULTWINDOW:4]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ELLSWIFT:yes EXTRAKEYS:yes MUSIG:yes RECOVERY:yes SCHNORRSIG:yes WIDEMUL:int128]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[ELLSWIFT:yes WIDEMUL:int128_struct]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[RECOVERY:yes WIDEMUL:int64]]) (push) Has been cancelled
CI / x86_64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[WIDEMUL:int128]]) (push) Has been cancelled
CI / i686: Linux (Debian stable) (clang --target=i686-pc-linux-gnu -isystem /usr/i686-linux-gnu/include, map[env_vars:map[]]) (push) Has been cancelled
CI / i686: Linux (Debian stable) (i686-linux-gnu-gcc, map[env_vars:map[]]) (push) Has been cancelled
CI / s390x (big-endian): Linux (Debian stable, QEMU) (map[env_vars:map[]]) (push) Has been cancelled
CI / ARM32: Linux (Debian stable, QEMU) (map[env_vars:map[ASM:arm32 EXPERIMENTAL:yes]]) (push) Has been cancelled
CI / ARM32: Linux (Debian stable, QEMU) (map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (clang, map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (clang-snapshot, map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (gcc, map[env_vars:map[]]) (push) Has been cancelled
CI / arm64: Linux (Debian stable) (gcc-snapshot, map[env_vars:map[]]) (push) Has been cancelled
CI / ppc64le: Linux (Debian stable, QEMU) (map[env_vars:map[]]) (push) Has been cancelled
CI / Valgrind arm64 (memcheck) (push) Has been cancelled
CI / Valgrind i686 (memcheck) (push) Has been cancelled
CI / Valgrind x64 (memcheck) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:auto CC:clang]]) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:auto CC:i686-linux-gnu-gcc HOST:i686-linux-gnu]]) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:no CC:clang ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / UBSan, ASan, LSan (map[env_vars:map[ASM:no CC:i686-linux-gnu-gcc ECMULTGENKB:2 ECMULTWINDOW:2 HOST:i686-linux-gnu]]) (push) Has been cancelled
CI / MSan (clang, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -fsanitize-memory-param-retval -g CTIMETESTS:no]]) (push) Has been cancelled
CI / MSan (clang, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g -O3 CTIMETESTS:yes ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / MSan (clang, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g CTIMETESTS:yes]]) (push) Has been cancelled
CI / MSan (clang-snapshot, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -fsanitize-memory-param-retval -g CTIMETESTS:no]]) (push) Has been cancelled
CI / MSan (clang-snapshot, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g -O3 CTIMETESTS:yes ECMULTGENKB:2 ECMULTWINDOW:2]]) (push) Has been cancelled
CI / MSan (clang-snapshot, map[env_vars:map[CFLAGS:-fsanitize=memory -fsanitize-recover=memory -g CTIMETESTS:yes]]) (push) Has been cancelled
CI / i686 (mingw32-w64): Windows (Debian stable, Wine) (push) Has been cancelled
CI / x86_64 (mingw32-w64): Windows (Debian stable, Wine) (push) Has been cancelled
CI / C++ -fpermissive (entire project) (map[env_vars:map[]]) (push) Has been cancelled
CI / C++ (public headers) (push) Has been cancelled

This commit is contained in:
Kgothatso Ngako
2026-09-01 00:31:57 +02:00
30 changed files with 9548 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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