diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f7527aa..f00c38b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ env: SCHNORRSIG_HALFAGG: 'no' FROST: 'no' CHILLDKG: 'no' + ICEBERG: 'no' ### test options SECP256K1_TEST_ITERS: 64 BENCH: 'yes' @@ -106,14 +107,14 @@ jobs: matrix: configuration: - env_vars: { WIDEMUL: 'int64', RECOVERY: 'yes' } - - env_vars: { WIDEMUL: 'int64', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes'} + - env_vars: { WIDEMUL: 'int64', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes'} - env_vars: { WIDEMUL: 'int128' } - env_vars: { WIDEMUL: 'int128_struct', ELLSWIFT: 'yes' } - env_vars: { WIDEMUL: 'int128', RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' } - - env_vars: { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes'} + - env_vars: { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes'} - env_vars: { WIDEMUL: 'int128', ASM: 'x86_64', ELLSWIFT: 'yes' } - env_vars: { RECOVERY: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes'} - - env_vars: { CTIMETESTS: 'no', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CPPFLAGS: '-DVERIFY' } + - env_vars: { CTIMETESTS: 'no', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CPPFLAGS: '-DVERIFY' } - env_vars: { BUILD: 'distcheck', WITH_VALGRIND: 'no', CTIMETESTS: 'no', BENCH: 'no' } - env_vars: { CPPFLAGS: '-DDETERMINISTIC' } - env_vars: { CFLAGS: '-O0', CTIMETESTS: 'no' } @@ -181,6 +182,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CC: ${{ matrix.cc }} steps: @@ -220,6 +222,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' steps: @@ -261,6 +264,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' steps: @@ -293,6 +297,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' CC: ${{ matrix.cc }} @@ -344,6 +349,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' steps: @@ -400,6 +406,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' SECP256K1_TEST_ITERS: 2 @@ -440,6 +447,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' CFLAGS: '-fsanitize=undefined,address -g' UBSAN_OPTIONS: 'print_stacktrace=1:halt_on_error=1' @@ -497,6 +505,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CC: ${{ matrix.cc }} SECP256K1_TEST_ITERS: 32 ASM: 'no' @@ -533,6 +542,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' CTIMETESTS: 'no' strategy: @@ -565,15 +575,15 @@ jobs: fail-fast: false matrix: env_vars: - - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes' } + - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes' } - { WIDEMUL: 'int128_struct', ECMULTGENKB: 2, ECMULTWINDOW: 4 } - - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes' } + - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes' } - { WIDEMUL: 'int128', RECOVERY: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CC: 'gcc' } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } - - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', CPPFLAGS: '-DVERIFY', CTIMETESTS: 'no' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CC: 'gcc' } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 } + - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', EXPERIMENTAL: 'yes', ECDSA_S2C: 'yes', RANGEPROOF: 'yes', SURJECTIONPROOF: 'yes', WHITELIST: 'yes', GENERATOR: 'yes', ECDSAADAPTOR: 'yes', BPPP: 'yes', SCHNORRSIG_HALFAGG: 'yes', FROST: 'yes', CHILLDKG: 'yes', ICEBERG: 'yes', CPPFLAGS: '-DVERIFY', CTIMETESTS: 'no' } - BUILD: 'distcheck' steps: @@ -751,6 +761,7 @@ jobs: SCHNORRSIG_HALFAGG: 'yes' FROST: 'yes' CHILLDKG: 'yes' + ICEBERG: 'yes' steps: - *CHECKOUT diff --git a/.gitignore b/.gitignore index cd3d8fd4..62334853 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ bench_generator bench_rangeproof bench_internal bench_whitelist +bench_iceberg noverify_tests tests exhaustive_tests @@ -57,6 +58,7 @@ contrib/gh-pr-create.sh frost_example chilldkg_example +iceberg_example ### CMake /CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 21378d99..f1fbb6e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ option(SECP256K1_ENABLE_MODULE_SCHNORRSIG "Enable schnorrsig module." ON) option(SECP256K1_ENABLE_MODULE_MUSIG "Enable musig module." ON) option(SECP256K1_ENABLE_MODULE_FROST "Enable FROST module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_CHILLDKG "Enable ChillDKG module (experimental)." OFF) +option(SECP256K1_ENABLE_MODULE_ICEBERG "Enable Iceberg threshold-MuSig module (experimental)." OFF) option(SECP256K1_ENABLE_MODULE_ELLSWIFT "Enable ElligatorSwift module." ON) option(SECP256K1_ENABLE_MODULE_GENERATOR "Enable NUMS generator module." ON) @@ -301,6 +302,7 @@ message(" schnorrsig .......................... ${SECP256K1_ENABLE_MODULE_SCHNO message(" musig ............................... ${SECP256K1_ENABLE_MODULE_MUSIG}") message(" frost ............................... ${SECP256K1_ENABLE_MODULE_FROST}") message(" chilldkg ............................ ${SECP256K1_ENABLE_MODULE_CHILLDKG}") +message(" iceberg ............................. ${SECP256K1_ENABLE_MODULE_ICEBERG}") message(" ElligatorSwift ...................... ${SECP256K1_ENABLE_MODULE_ELLSWIFT}") message(" generator ........................... ${SECP256K1_ENABLE_MODULE_GENERATOR}") message(" rangeproof .......................... ${SECP256K1_ENABLE_MODULE_RANGEPROOF}") diff --git a/Makefile.am b/Makefile.am index 9437e648..4deb521b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -118,6 +118,12 @@ bench_internal_CPPFLAGS = $(SECP_CONFIG_DEFINES) bench_ecmult_SOURCES = src/bench_ecmult.c bench_ecmult_LDADD = $(COMMON_LIB) $(PRECOMPUTED_LIB) bench_ecmult_CPPFLAGS = $(SECP_CONFIG_DEFINES) +if ENABLE_MODULE_ICEBERG +noinst_PROGRAMS += bench_iceberg +bench_iceberg_SOURCES = src/bench_iceberg.c +bench_iceberg_LDADD = $(COMMON_LIB) $(PRECOMPUTED_LIB) +bench_iceberg_CPPFLAGS = $(SECP_CONFIG_DEFINES) +endif endif TESTS = @@ -234,6 +240,17 @@ chilldkg_example_LDFLAGS += -lbcrypt endif TESTS += chilldkg_example endif +if ENABLE_MODULE_ICEBERG +noinst_PROGRAMS += iceberg_example +iceberg_example_SOURCES = examples/iceberg.c +iceberg_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC +iceberg_example_LDADD = libsecp256k1.la +iceberg_example_LDFLAGS = -static +if BUILD_WINDOWS +iceberg_example_LDFLAGS += -lbcrypt +endif +TESTS += iceberg_example +endif endif ### Precomputed tables @@ -378,3 +395,7 @@ endif if ENABLE_MODULE_CHILLDKG include src/modules/chilldkg/Makefile.am.include endif + +if ENABLE_MODULE_ICEBERG +include src/modules/iceberg/Makefile.am.include +endif diff --git a/README.md b/README.md index 4e211304..68ebc17b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Added features: * Experimental module for Schnorr signature half-aggregation. * Experimental module for [FROST (BIP 445)](src/modules/frost/frost.md). * Experimental module for [ChillDKG](src/modules/chilldkg/chilldkg.md), distributed key generation for FROST (bip-frost-dkg draft). +* Experimental module for [Iceberg](doc/iceberg.md), a threshold scheme that lets a group of parties stand in for a single MuSig2 (BIP 327) participant. Experimental features are made available for testing and review by the community. The APIs of these features should not be considered stable. diff --git a/ci/ci.sh b/ci/ci.sh index 5dbb9604..e3bab1a6 100755 --- a/ci/ci.sh +++ b/ci/ci.sh @@ -15,7 +15,7 @@ print_environment() { ECMULTWINDOW ECMULTGENKB ASM WIDEMUL WITH_VALGRIND EXTRAFLAGS \ EXPERIMENTAL ECDH RECOVERY EXTRAKEYS SCHNORRSIG MUSIG SCHNORRSIG_HALFAGG ELLSWIFT \ ECDSA_S2C GENERATOR RANGEPROOF SURJECTIONPROOF WHITELIST ECDSAADAPTOR BPPP \ - FROST CHILLDKG SECP256K1_TEST_ITERS BENCH SECP256K1_BENCH_ITERS CTIMETESTS SYMBOL_CHECK \ + FROST CHILLDKG ICEBERG SECP256K1_TEST_ITERS BENCH SECP256K1_BENCH_ITERS CTIMETESTS SYMBOL_CHECK \ EXAMPLES \ HOST WRAPPER_CMD \ CC CFLAGS CPPFLAGS AR NM \ @@ -71,6 +71,7 @@ fi --enable-module-schnorrsig-halfagg="$SCHNORRSIG_HALFAGG" \ --enable-module-frost="$FROST" \ --enable-module-chilldkg="$CHILLDKG" \ + --enable-module-iceberg="$ICEBERG" \ --enable-examples="$EXAMPLES" \ --enable-ctime-tests="$CTIMETESTS" \ --with-valgrind="$WITH_VALGRIND" \ @@ -134,6 +135,10 @@ then then $EXEC ./bench_bppp fi + if [ "$ICEBERG" = "yes" ] + then + $EXEC ./bench_iceberg + fi } >> bench.log 2>&1 fi diff --git a/configure.ac b/configure.ac index 4e2f70c1..6dec492e 100644 --- a/configure.ac +++ b/configure.ac @@ -250,6 +250,11 @@ AC_ARG_ENABLE(module_chilldkg, [], [SECP_SET_DEFAULT([enable_module_chilldkg], [no], [yes])]) +AC_ARG_ENABLE(module_iceberg, + AS_HELP_STRING([--enable-module-iceberg],[enable Iceberg threshold-MuSig module (experimental)]), + [], + [SECP_SET_DEFAULT([enable_module_iceberg], [no], [yes])]) + # Test-only override of the (autodetected by the C code) "widemul" setting. # Legal values are: # * int64 (for [u]int64_t), @@ -568,6 +573,14 @@ if test x"$enable_module_chilldkg" = x"yes"; then enable_module_ecdh=yes fi +if test x"$enable_module_iceberg" = x"yes"; then + if test x"$enable_module_musig" = x"no"; then + AC_MSG_ERROR([Module dependency error: You have disabled the musig module explicitly, but it is required by the iceberg module.]) + fi + enable_module_musig=yes + SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_ICEBERG=1" +fi + if test x"$enable_external_default_callbacks" = x"yes"; then SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DUSE_EXTERNAL_DEFAULT_CALLBACKS=1" fi @@ -618,6 +631,9 @@ if test x"$enable_experimental" = x"no"; then if test x"$enable_module_chilldkg" = x"yes"; then AC_MSG_ERROR([ChillDKG module is experimental. Use --enable-experimental to allow.]) fi + if test x"$enable_module_iceberg" = x"yes"; then + AC_MSG_ERROR([Iceberg module is experimental. Use --enable-experimental to allow.]) + fi fi # Check for concurrency support (tests only) @@ -657,6 +673,7 @@ AM_CONDITIONAL([ENABLE_MODULE_BPPP], [test x"$enable_module_bppp" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_SCHNORRSIG_HALFAGG], [test x"$enable_module_schnorrsig_halfagg" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_FROST], [test x"$enable_module_frost" = x"yes"]) AM_CONDITIONAL([ENABLE_MODULE_CHILLDKG], [test x"$enable_module_chilldkg" = x"yes"]) +AM_CONDITIONAL([ENABLE_MODULE_ICEBERG], [test x"$enable_module_iceberg" = x"yes"]) AM_CONDITIONAL([USE_REDUCED_SURJECTION_PROOF_SIZE], [test x"$use_reduced_surjection_proof_size" = x"yes"]) AM_CONDITIONAL([USE_EXTERNAL_ASM], [test x"$enable_external_asm" = x"yes"]) AM_CONDITIONAL([USE_ASM_ARM], [test x"$set_asm" = x"arm32"]) @@ -700,6 +717,7 @@ echo " module bppp = $enable_module_bppp" echo " module schnorrsig-halfagg = $enable_module_schnorrsig_halfagg" echo " module frost = $enable_module_frost" echo " module chilldkg = $enable_module_chilldkg" +echo " module iceberg = $enable_module_iceberg" echo echo " asm = $set_asm" echo " ecmult window size = $set_ecmult_window" diff --git a/doc/iceberg.md b/doc/iceberg.md new file mode 100644 index 00000000..ae7fed54 --- /dev/null +++ b/doc/iceberg.md @@ -0,0 +1,441 @@ +Notes on the iceberg module API +=============================== + +The following sections contain additional notes on the API of the iceberg +module (`include/secp256k1_iceberg.h`). A usage example can be found in +`examples/iceberg.c`, which runs the whole flow and narrates it. + +**This module is experimental.** It builds by default here, which is a +development convenience rather than a statement that it is ready. Iceberg has a +security proof, by reduction to NestedMuSig2's unforgeability, but it is in an +anonymous conference submission that is still a working draft, and at the two +nonces BIP-327 fixes that reduction holds in the algebraic group model rather +than the plain random oracle model. The proof also assumes a property no library +can provide (that a session label is used once, group-wide) and the known +ways to lose a key all live in exactly that assumption. Do not put money behind +this module. + +Iceberg lets a *t*-of-*n* group act as a single MuSig2 participant. From outside, +the result is an ordinary BIP-340 signature: nothing in it records that a group +was involved, or how large the group was. + +## The objects + +Two APIs are in play and about a dozen nouns between them. MuSig2's, which this +module does not replace: + +| name | type | made by | secret? | size | +|---|---|---|---|---| +| key aggregation cache | `musig_keyagg_cache` | anyone, from the signers' public keys | no | opaque | +| secret nonce | `musig_secnonce` | a signer, once per session | **yes, and it must survive between the rounds** | never serialized | +| public nonce | `musig_pubnonce` | a signer | no | 66 B | +| aggregate nonce | `musig_aggnonce` | anyone | no | 66 B | +| partial signature | `musig_partial_sig` | a signer | no | 32 B | + +And Iceberg's, which exist entirely inside the group: + +| name | type | made by | secret? | size | +|---|---|---|---|---| +| share | `iceberg_share` | the dealer, once | **yes, and it is the only _secret_ anyone stores** | 4 + 32*C(n-1, t-1) B | +| share cache | `iceberg_share_cache` | a participant | no: Lagrange weights, which depend only on which participant you are | opaque | +| public share | `iceberg_pubshare` | a participant | no | 34 B | +| nonce contribution | `iceberg_pubnonce` | a participant, per session | no | 67 B | +| group nonce | `iceberg_aggnonce` | nobody has to: no call takes one as input, so pass `NULL` unless you want it for logging | no | 66 B | +| signature share | `iceberg_partial_sig` | a participant, per session | no | 33 B | + +Two names are close and the objects are not. The group produces a MuSig2 **partial signature**, built +out of Iceberg **signature shares**, one per participant. Likewise a participant +makes a **nonce contribution**, and the group turns `2t-1` of them into one +ordinary MuSig2 **public nonce**. + +Those two conversions are the seam. `iceberg_nonce_agg` emits a +`musig_pubnonce` and `iceberg_partial_sig_agg` emits a `musig_partial_sig`; +everything above them is ordinary MuSig2 that knows nothing about a group, and +everything below is this module. + +## Three roles + +Three roles appear here, and they run different code: + +| role | runs | trusted? | +|---|---|---| +| **participant** | `iceberg_*` calls. Holds a share, never the key. There are *n* of them | with its own share only | +| **coordinator** | moves messages, calls the `_agg` functions | **no.** Every check in the module assumes it is hostile | +| **cosigner** | plain `musig_*` calls, and knows nothing about the group | as any MuSig2 signer | + +A participant may also act as the coordinator; nothing changes if it does, +because the coordinator has no privileges to abuse. + +## How many people, and when + +Two different counts: + +- **Round one needs `2t-1` participants.** Not because the secret needs that + many, but because the group verifies its own nonce contributions, and that + check is error detection. + + A contribution is a point on a polynomial of degree `t-1`, and **any `t` + points lie on some polynomial of that degree**, including `t` an adversary + chose. So a quorum of `t` does not weaken the check, it empties it. Every + point past `t` is one more constraint a liar has to satisfy, and outnumbering + `t-1` liars takes `t-1` of them: + + t + (t-1) = 2t-1 + + Read as coding theory it is the same statement: detecting `e` errors in a code + of dimension `t` needs `t+e` symbols. The `3t-2` under "Not implemented" is + this formula with correction, `t+2e`, in place of detection. Note that the + `n >= 3t-2` deployment bound below is a *different* result that happens to be + the same number: one is Reed-Solomon correction, the other is Byzantine + agreement, and neither implies the other. +- **Round two needs `2t-1` too**, and they need not be the ones who took part in + round one. The paper's Table II gives the signing quorum as `2t-1` online + members for every threshold, and the honest majority that number represents + does not stop applying halfway through a session. + + A member that was offline for round one can still take part: it holds the + share that determines what its contribution would have been, so it can verify + the set it is handed and sign against it. That is the property the + deterministic nonces exist to buy, and it is why `partial_sign` does not + insist on finding your own contribution in the set; see "API misuse". + + `t` appears in round two only as the interpolation degree; the arithmetic + that turns signature shares back into one signature needs `t` points. Reading + that as "round two needs `t` people" is the mistake to avoid. + +Since `2t-1` participants must exist, `2t-1 <= n`, so: + + t <= (n+1)/2 + +This is a hard structural limit, not a performance note. **2-of-2, 3-of-3, +3-of-4, 4-of-5, 4-of-6 and 6-of-10 cannot be expressed at all.** +`secp256k1_iceberg_shares_gen` refuses them at setup rather than mishandling +them later; see the failure table under "API misuse" for what refusing means. + +Sizes, since a share is a bundle of seeds and grows quickly. Both rounds want +`2t-1` members online; the third column is the smaller number the *arithmetic* of +step 10 needs, which is a degree and not a quorum. + +**These are expressible configurations, not recommended ones.** A separate bound +applies to deployment: agreeing on the live state with up to `t-1` faulty members +is Byzantine agreement, which needs `n >= 3(t-1)+1 = 3t-2`. That gives 2-of-4, +3-of-7, 4-of-10, 5-of-13 as the smallest deployable groups, and **five of the +seven rows below fall short of it**. The signature scheme is correct at all of +them, since the arithmetic does not know how many faults the surrounding +agreement survives, but a group sized from this table alone will be too small for the +consensus the scheme assumes. See the deployment constraints in +`include/secp256k1_iceberg.h`. + +Note where the two bounds meet. 5-of-13 needs more participants than +`SECP256K1_ICEBERG_MAX_PARTICIPANTS` allows, so at the maximum of 10 the +largest threshold that is both expressible and deployable is **4-of-10**. The +table below goes past that line, and so do the benchmark and the tests: 5-of-9 +and 5-of-10 are there to show the cost curve, not because a group should be +sized that way. + +`SECP256K1_ICEBERG_MAX_PARTICIPANTS` is 10. It sizes two of the opaque types and +the largest stack frames in the module, all of which grow as `C(n-1, t-1)`, so it +is part of the ABI and not something one member of a group changes on its own. A +build that will only ever run small groups can lower it, and gets a 200-byte share +instead of a 4040-byte one at five participants; `src/modules/iceberg/rss.h` +beside the `#error` says which three derived values have to be lowered with it, +and `run_iceberg_binom_test` recomputes all three and names the one that is wrong. +Raising it is refused at compile time. `rss.h` also carries the command that +measures the stack frames, rather than a number, because the number moves with the +compiler. None of that is in the public header: a caller of the installed library +has no `rss.h`, no test suite and no `src/` to run it against. + +| config | quorum `2t-1` | shares that interpolate | seeds in the group | seeds per participant | serialized share | +|---|---|---|---|---|---| +| 2-of-3 | 3 | 2 | 3 | 2 | 68 B | +| 2-of-4 | 3 | 2 | 4 | 3 | 100 B | +| 3-of-5 | 5 | 3 | 10 | 6 | 196 B | +| 3-of-7 | 5 | 3 | 21 | 15 | 484 B | +| 4-of-7 | 7 | 4 | 35 | 20 | 644 B | +| 5-of-9 | 9 | 5 | 126 | 70 | 2244 B | +| 5-of-10 | 9 | 5 | 210 | 126 | 4036 B | + +Everything that crosses the network serializes: a share to `4 + 32*C(n-1, t-1)` +bytes, a public share to 34, a nonce contribution to 67 and a signature share to +33. The group's aggregate nonce serializes to 66 as well, but it is not on that +list: nothing receives one. Those bytes are the start of the b1 preimage, and +they are there for an implementation checking its arithmetic against this one. + +The public share, the nonce contribution and the signature share each carry the +participant index they belong to, which is the extra byte in each. The group's +aggregate nonce does not, since it belongs to the group rather than to a member, +which is exactly why it is 66 bytes and a contribution is 67. + +The group's seed count is `C(n, t-1)` and each participant holds +`C(n-1, t-1)` of them. The bounds derived from +`SECP256K1_ICEBERG_MAX_PARTICIPANTS` (`SECP256K1_ICEBERG_MAX_SEEDS` here, and +`MAX_T` and `MAX_SUBSETS` in `rss.h`) are written out by hand because C89 +cannot evaluate a binomial at preprocessing time. The test suite recomputes each +of them and fails on a wrong line. + +## The flow + + participant k coordinator cosigner + (one of n) (untrusted) (plain musig2) + ------------- ----------- -------------- + + SETUP, once. A dealer runs shares_gen and is then not needed again. + iceberg_shares_gen -> one share each [_dealer.h, not installed] + iceberg_pubshare_gen -> iceberg_pubkey_agg -> the group's public key + + KEY AGGREGATION. The group is now one public key among several. + musig_pubkey_agg([group_pk, cosigner_pk, ...]) -> keyagg_cache + optionally musig_pubkey_xonly_tweak_add / _ec_tweak_add + + === ROUND ONE ================================ needs 2t-1 participants === + + Neither side waits on the other. iceberg_nonce_gen needs only the share + and the label; musig_nonce_gen is an ordinary MuSig2 call and takes + whatever it usually takes. Drawn side by side for that reason. + + iceberg_nonce_gen(share, sid32) musig_nonce_gen + --- pubnonce (67 B) --> + <-- pubnonce ------ + iceberg_nonce_agg + verifies 2t-1 of them, interpolates + -> one ordinary musig pubnonce + musig_nonce_agg([group, cosigners]) + --- aggnonce --> + musig_nonce_process + + ------------------------------------------------------------------------ + THE GAP. No secret nonce survives this line. A participant's nonces + are a function of (share, sid), and it is handed the sid again, so a + crash here costs it nothing and there is no secret to lose, leak or + duplicate. That is narrower than "stores nothing": it must still + remember which labels it has answered under, and restoring a backup + from before that record is exactly the exploitable case. + ------------------------------------------------------------------------ + + === ROUND TWO ============================ needs 2t-1 again, any of n === + + The message appears here for the first time, which is the point: the + group commits to a nonce before knowing what it will sign. + + --- msg, the round-one pubnonces --> + iceberg_partial_sign(share, sid32, msg, the contributions, ...) + derives the group's aggregate from the contributions rather than + accepting one, checks they are a single sharing of degree t-1, and + compares their value at its own index against the contribution it + derives for itself. So it signs against the aggregate the group + really formed. A member that sat round one out can still sign. + --- partial sig (33 B) --> + iceberg_partial_sig_agg + -> one ordinary musig partial sig + musig_partial_sign + <-- partial sig --- + musig_partial_sig_agg -> 64-byte signature + + schnorrsig_verify accepts it under the (possibly tweaked) aggregate key. + +## Signing, as a list + +1. **Dealer**: `secp256k1_iceberg_shares_gen`, from + `include/secp256k1_iceberg_dealer.h`, then hand each participant its share + and forget the seed. That header is separate and is not installed, because a + trusted dealer is not something the library offers for deployment; see + "API misuse" below. A distributed key generation replaces this step and + nothing downstream changes. +2. **Each participant**: `secp256k1_iceberg_pubshare_gen`, publish the result. +3. **Anyone**: `secp256k1_iceberg_pubkey_agg` over any `2t-1` public shares. + This checks they agree, so a participant that published a wrong one is + caught here rather than at signing time. +4. **Anyone**: `secp256k1_musig_pubkey_agg` with the group's public key and the + cosigners', then optionally `secp256k1_musig_pubkey_xonly_tweak_add` and + `secp256k1_musig_pubkey_ec_tweak_add`. +5. **Each of `2t-1` participants**: `secp256k1_iceberg_nonce_gen`, over the + session label alone. Publish the result. Nothing here depends on the + message or on the cosigners, so this can run before either exists. +6. **Cosigners**, independently and in either order: `secp256k1_musig_nonce_gen`, + then `secp256k1_musig_nonce_agg` over the cosigners' nonces alone. +7. **Coordinator**: `secp256k1_iceberg_nonce_agg`, which verifies and + interpolates them into one ordinary MuSig2 public nonce. +8. **Coordinator**: `secp256k1_musig_nonce_agg` over that and the cosigners', + then `secp256k1_musig_nonce_process`. +9. **Participants**, not necessarily the ones from step 5: + `secp256k1_iceberg_partial_sign`, given the group's own round-one + contributions, not an aggregate of them and not the cosigners', the same + label, and now the message. This is where the message enters and the only + place it appears in the API. Each call needs all `2t-1` contributions from + step 5 and returns 0 with fewer, and the scheme wants `2t-1` members online + here as in round one; step 10 then interpolates from as few as `t` of the + resulting shares. Before calling it, each signer checks its own record that it + has not answered under this label already; the library cannot do that for it, + because it holds nothing between calls. +10. **Coordinator**: `secp256k1_iceberg_partial_sig_agg`, giving one MuSig2 + partial signature. Hand it more than `t` shares and it degree-checks them, + which above the threshold catches a set that contradicts itself; at exactly + `t` there is nothing to disagree with. +11. **Cosigners**: `secp256k1_musig_partial_sign` as usual. +12. **Coordinator**: `secp256k1_musig_partial_sig_agg`, then + `secp256k1_schnorrsig_verify`. + +Steps 1 to 4 happen once per group, and `secp256k1_iceberg_keyagg_check` belongs +with step 4: it confirms the outer cache aggregates the key list you think it +does, which is a fact about the channel rather than about this attempt. Steps 5 +to 12 are one signing session. + +## API misuse + +The musig module's three rules apply here too: unique nonces, never copy or +serialize a `secp256k1_musig_secnonce`, and never read or write an opaque struct +directly. Iceberg adds its own, and the reasons are specific. + +**How a call refuses, before anything else.** A function returns 0 when the +values it was handed do not work together: an inconsistent set of shares, a +malformed encoding, a contribution derived under some other label. The illegal +callback, which aborts the process unless the caller has installed its own with +`secp256k1_context_set_illegal_callback`, is reserved for a bug in the calling +code: a null pointer, an uninitialized opaque struct, a group shape the scheme +cannot express. + +How many contributions turned up is neither. It is a fact about the group, +influenced by whichever peers answered, so every call that takes a count returns +0 rather than aborting: + +| call | a bad count does what | +|---|---| +| `iceberg_shares_gen` | aborts: `n` outside `1..10`, or `t` outside `1..(n+1)/2`. These are the group's shape, not a count | +| `iceberg_pubkey_agg` | returns 0: fewer than `2t-1` public shares, or more than `n` | +| `iceberg_nonce_agg` | returns 0: fewer than `2t-1` contributions, or more than `n` | +| `iceberg_partial_sig_agg` | returns 0: fewer than `t` shares or more than `n`. A share that never came from `partial_sign` still aborts, that one being an uninitialized struct | +| `iceberg_partial_sign` | returns 0: it checks the same `2t-1` bound while deriving the aggregate | +| `iceberg_partial_sig_verify` | returns 0: same reason, and it derives the same aggregate | + +A member that waits for a deadline and aggregates whatever arrived is therefore +doing something the API supports, rather than something that kills its process +the first time a peer is asleep. + +**Where n and t come from.** `iceberg_partial_sign` reads them off the caller's +own share and never asks. The four calls that take them as arguments should be +given the same values; a member that keeps only a serialized share can read them +from it, since the encoding is `version | n | t | index | seeds`. It matters most +for `t`: understate it and the degree check still runs, against a lower degree, +and proves less. Nothing cross-checks the four calls against each other. + +**You supply the session label, and the rule about it is yours to enforce.** Every participant's secret nonces are a +deterministic function of its seeds and the label, and the seeds never change, +so a label reused under two different messages produces two answers in which +`k1`, `k2` and `d` are identical while `b0` and `e` have moved. Three such +answers are three linear equations in those three unknowns, and solving them +recovers a key share. + +Concretely, the caller must guarantee both halves: + +- **one answer per member per label.** A member can enforce this alone, but not + from anything the library holds; it keeps nothing between calls. It needs + durable storage of its own: one 32-byte field per participant holding the last + label it signed under, and a rule that a new label must be strictly greater. + `may_sign_under` in `examples/iceberg.c` does exactly that. + Restore it with the share; a record rolled back to an old backup is a member + that will answer twice. +- **one message per label across the whole group**: the members must agree on + what they are signing before any of them answers. + +The library sees one call at a time and holds nothing between them, so it can +enforce neither. Neither can any +individual participant: a coordinator can show three *different* members three +*different* messages under one label, and each one signs exactly once, refuses +nothing, and detects nothing, because nothing in the protocol tells a member +that somebody else saw this label too. **Per-participant discipline is therefore +not a substitute for group agreement**, which is why the scheme's security model +assumes a consensus its deployment already runs. + +*Why the label cannot simply be derived from the message, which would close all +of this:* round one has to run before the message exists. In Lightning the +nonce is exchanged a full round-trip before the commitment transaction is +assembled, so a label binding the message could not be computed when it is +needed. The intended label is the channel's commitment number, plus a counter +for retries under it. A label bound to the message is possible where the message +*is* known early, but it costs the message-independent first round, which is a different setting from the one this +module assumes. The module takes any 32 bytes and offers no derivation, because +the right label depends on the deployment, and the wrong one costs a key share. + +**`secp256k1_iceberg_partial_sign` takes the group's own round-one +contributions, not an aggregate of them.** This is not an optimisation +opportunity. The nesting coefficient is a hash of the group's aggregate nonce, +so a coordinator free to invent that aggregate gets a coefficient it can vary at +will: three invented aggregates under one correctly-bound label again give +three equations in the same three unknowns. The aggregate is therefore derived +from contributions that must pass the degree check together, and then the +interpolated polynomial is evaluated at the signer's own index to get the nonce +shares it signs with. It signs against the aggregate the group actually formed, +whether or not it was one of the members who helped form it. + +It does *not* require the signer's own round-one contribution to be present and +unaltered in the set. That would be strictly stronger against an unauthenticated +transport, and it would lock out a member who was offline during round one -- +the exact failure the deterministic nonces exist to survive. With the +authenticated transport the scheme assumes anyway it is unnecessary: among +`2t-1` contributions with at most `t-1` corruptions, at least `t` are honest, +and `t` points already pin a degree `t-1` polynomial. + +**A tweak belongs to the outer session and must not be applied twice.** The +module is correct here and needs nothing from the caller. No Iceberg call takes +a tweak because `secp256k1_musig_nonce_process` sets +the tweak term aside and `secp256k1_musig_partial_sig_agg` adds it in once, at +the top. The group's shares carry the key coefficient and not the tweak. + +**`secp256k1_iceberg_shares_gen` is a trusted dealer, and lives outside the +installed API for that reason.** For the duration of that one call, one machine +holds enough to reconstruct the group's private key, which is the situation a +threshold scheme exists to avoid. It is fine for testing, and fine where one +party is already trusted with the whole key. It is not fine otherwise, and this +module does not provide a distributed key generation. + +It is declared in `include/secp256k1_iceberg_dealer.h`, which is in the tree so +the tests, the benchmarks and the example have shares to work with, and is not +installed. Including `secp256k1_iceberg.h` does not offer you a dealer. Nothing +in the signing API cares how a share was produced: one arrives through +`secp256k1_iceberg_share_parse`, so a share from a DKG or from another +implementation is used identically. + +## What is stored, and for how long + +| object | who holds it | lifetime | secret? | +|---|---|---|---| +| `iceberg_share` | one participant | forever | **yes** | +| `iceberg_share_cache` | one participant | optional, derived from the share | no | +| `iceberg_pubshare` | published | forever | no | +| session label (`sid32`) | anyone | one session, and a record that it was used, for as long as the group lives | no | +| `iceberg_pubnonce` | published | one session | no | +| `iceberg_aggnonce` | coordinator | one session | no | +| `iceberg_partial_sig` | published | one session | no | + +Nothing marked secret above needs to survive a reboot except the share, +which is why `examples/iceberg.c` wipes every participant between the two rounds +and rebuilds them from storage. That is the property Iceberg exists for: a FROST +signer must keep a secret nonce alive across the same gap, and losing it, +restoring an old copy over it, or running two instances of the signer are all +catastrophic. + +It does not follow that a participant is stateless, and the table does not say +so. A member must also remember **which labels it has already answered under**. +That record is not secret and so is not listed above, but it is not optional: restoring a backup taken before it +was written is the one restore that is dangerous. A signer with no memory can +also be talked into signing a superseded channel state, which in Lightning costs +it the channel. + +The share cache holds no secrets despite being derived from a share: it is +Lagrange weights, which depend only on which participant it is. It has no +serialized form and does not need one; passing `NULL` wherever a cache is +accepted rebuilds it. `bench_iceberg` prints what that costs, along with the +rest of the module; the figure moves with the machine. + +## Not implemented + +- **Naming the liar.** `secp256k1_iceberg_partial_sig_verify` ships, so a share + can be checked against the commitments it claims to come from, and a failed + signature need not be a mystery. What it does not do is assign blame: a 0 + means that share does not satisfy the equation, not that its author cheated, + because MuSig2 partial signatures are forgeable. Naming the liar is error + *correction* rather than detection, and correction needs `t+2e` points where + detection needed `t+e`, so `3t-2` online at once, against `2t-1` for + signing. That is usually more people than the group has, which is why the + function is documented as detection and stops there. +- **Distributed key generation.** See above. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 50795899..d04e6645 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -39,3 +39,7 @@ endif() if(SECP256K1_ENABLE_MODULE_CHILLDKG) add_example(chilldkg) endif() + +if(SECP256K1_ENABLE_MODULE_ICEBERG) + add_example(iceberg) +endif() diff --git a/examples/iceberg.c b/examples/iceberg.c new file mode 100644 index 00000000..9e1ec935 --- /dev/null +++ b/examples/iceberg.c @@ -0,0 +1,725 @@ +/************************************************************************* + * To the extent possible under law, the author(s) have dedicated all * + * copyright and related and neighboring rights to the software in this * + * file to the public domain worldwide. This software is distributed * + * without any warranty. For the CC0 Public Domain Dedication, see * + * EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 * + *************************************************************************/ + +/** A 3-of-7 group signing beside an ordinary MuSig2 cosigner. + * + * The group behaves as one MuSig2 participant, and the finished signature is + * an ordinary BIP-340 signature that records nothing about the group. + * + * Three roles appear below and they run different code, which is the thing + * worth keeping straight while reading: + * + * participant holds a share, produces a nonce and a signature share, and + * never sees the whole key. There are seven of them. + * coordinator moves messages around and combines them. Untrusted: every + * check here assumes it is hostile. + * cosigner an ordinary MuSig2 signer that knows nothing about any of + * this and calls the plain musig API. + * + * The participants are wiped between the two rounds and rebuilt from storage, + * because no secret nonce survives that gap; see reboot_participants for what + * does survive it, which is not nothing. One call is made that is expected to + * fail, in refusals_are_refused. + * + * See also include/secp256k1_iceberg.h and doc/iceberg.md. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +/* The trusted dealer lives in its own header, and is not part of the installed + * API. See the note at the top of it. */ +#include + +#include "examples_util.h" + +#define N 7 /* participants in the group */ +#define T 3 /* how many of them can sign */ +#define MU (2 * T - 1) /* how many must take part in round one */ + +/* One participant's memory: one secret, and one note to itself. + * + * The share is the secret and never changes. `answered` is the highest session + * label this participant has signed under. The library does not know about it and + * could not, since it holds nothing between calls, but answering twice under one + * label gives away the key share, so somebody has to remember, and the somebody + * is the caller. */ +struct participant { + secp256k1_iceberg_share share; + unsigned char answered[32]; +}; + +/* The same participant's disk. A reboot loses the struct above, not this one, + * and both fields have to come back. See reboot_participants. */ +struct storage { + unsigned char share_bytes[SECP256K1_ICEBERG_SHARE_MAX_LEN]; + size_t share_len; + unsigned char answered_bytes[32]; +}; + +/* The threshold side. Set up once, then unchanged for the life of the group. */ +struct group { + struct participant member[N]; + struct storage disk[N]; + secp256k1_iceberg_pubshare pubshare[N]; + secp256k1_pubkey pubkey; +}; + +/* The other side: an ordinary MuSig2 signer, which knows nothing about any of + * the above. Everything it holds is its own, including the secret nonce that + * the group deliberately does not have an equivalent of. */ +struct cosigner { + secp256k1_keypair keypair; + secp256k1_pubkey pubkey; + secp256k1_musig_secnonce secnonce; +}; + +/* What the two of them add up to. Computed once, at key aggregation, and the + * only thing a verifier ever sees. */ +struct shared_key { + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_xonly_pubkey output; +}; + +/* One attempt at one signature. Everything here is public. */ +struct signing_session { + unsigned char sid[32]; + secp256k1_iceberg_pubnonce contribution[MU]; + const secp256k1_iceberg_pubnonce *contribution_ptr[MU]; + secp256k1_musig_pubnonce group_nonce; /* the group's, after interpolating */ + secp256k1_musig_pubnonce cosigner_nonce; /* the cosigner's, as published */ + secp256k1_musig_aggnonce cosigner_aggnonce; /* the cosigners' alone */ + secp256k1_musig_session musig_session; +}; + +static void heading(const char *text) { + printf("\n%s\n", text); +} + +static void step(const char *text) { + printf(" %-55s", text); + fflush(stdout); +} + +/* Deal the shares and publish the group's key. + * + * This is a trusted dealer: for the length of one call, this machine holds + * enough to reconstruct the group's private key. Acceptable for testing, and + * where one party is trusted already. A distributed key generation produces + * the same shares without that moment ever existing, and this module does not + * provide one. */ +static int deal_shares(const secp256k1_context *ctx, struct group *group) { + secp256k1_iceberg_share *share_ptr[N]; + const secp256k1_iceberg_pubshare *pubshare_ptr[N]; + unsigned char seed[32]; + unsigned int k; + int ok; + + step("Dealing shares (secp256k1_iceberg_shares_gen)"); + if (!fill_random(seed, sizeof(seed))) { + return 0; + } + for (k = 0; k < N; k++) { + share_ptr[k] = &group->member[k].share; + } + ok = secp256k1_iceberg_shares_gen(ctx, share_ptr, N, T, seed); + secure_erase(seed, sizeof(seed)); + if (!ok) { + return 0; + } + printf("ok\n"); + + /* A share is one seed per (t-1)-subset that leaves this participant out, + * so it is large and grows quickly with the group. Each participant keeps + * its own and nothing else. */ + step("Storing them (secp256k1_iceberg_share_serialize)"); + for (k = 0; k < N; k++) { + group->disk[k].share_len = sizeof(group->disk[k].share_bytes); + if (!secp256k1_iceberg_share_serialize(ctx, group->disk[k].share_bytes, + &group->disk[k].share_len, &group->member[k].share)) { + return 0; + } + /* Nothing answered yet, so the lowest possible label. Do this once, at + * dealing; doing it again later throws the protection away. */ + memset(group->member[k].answered, 0, sizeof(group->member[k].answered)); + memcpy(group->disk[k].answered_bytes, group->member[k].answered, 32); + } + printf("ok, %lu bytes each\n", (unsigned long)group->disk[0].share_len); + + step("Group public key (secp256k1_iceberg_pubkey_agg)"); + for (k = 0; k < N; k++) { + if (!secp256k1_iceberg_pubshare_gen(ctx, &group->pubshare[k], &group->member[k].share, NULL)) { + return 0; + } + pubshare_ptr[k] = &group->pubshare[k]; + } + /* 2t-1 public shares are more than the key needs, since t of them already + * determine it, and the surplus is the point: they have to agree, so a + * participant that published a wrong one is caught now rather than at + * signing time. */ + if (!secp256k1_iceberg_pubkey_agg(ctx, &group->pubkey, pubshare_ptr, MU, N, T)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Aggregate the group's key with the cosigner's, and tweak the result. + * + * Nothing below this point knows that one of the two keys is a group. The + * tweak belongs to this outer session: musig_nonce_process sets its term aside + * and musig_partial_sig_agg adds it once, at the top. No Iceberg call is even + * told the tweak happened, which is what stops it being counted twice. */ +static int aggregate_keys(const secp256k1_context *ctx, struct group *group, struct cosigner *cosigner, struct shared_key *shared) { + const secp256k1_pubkey *pubkeys[2]; + secp256k1_pubkey output_pk; + unsigned char taptweak[32]; + unsigned char seckey[32]; + int ok; + + step("Cosigner keypair (secp256k1_keypair_create)"); + ok = fill_random(seckey, sizeof(seckey)) + && secp256k1_keypair_create(ctx, &cosigner->keypair, seckey) + && secp256k1_keypair_pub(ctx, &cosigner->pubkey, &cosigner->keypair); + /* The keypair holds everything needed from here on, so the raw key does not + * outlive this call; secp256k1_keypair_sec brings it back when required. */ + secure_erase(seckey, sizeof(seckey)); + if (!ok) { + return 0; + } + printf("ok\n"); + + step("Aggregating (secp256k1_musig_pubkey_agg)"); + pubkeys[0] = &group->pubkey; + pubkeys[1] = &cosigner->pubkey; + if (!secp256k1_musig_pubkey_agg(ctx, NULL, &shared->keyagg_cache, pubkeys, 2)) { + return 0; + } + printf("ok\n"); + + /* The cache records the hash of the key list, not the list, so nothing ties + * it to the group's key later. Run this once, here, rather than trusting a + * cache round two cannot check. */ + step("Checking the cache (secp256k1_iceberg_keyagg_check)"); + if (!secp256k1_iceberg_keyagg_check(ctx, &shared->keyagg_cache, pubkeys, 2, + &group->pubkey)) { + return 0; + } + printf("ok\n"); + + step("Tweaking (secp256k1_musig_pubkey_xonly_tweak_add)"); + if (!fill_random(taptweak, sizeof(taptweak)) + || !secp256k1_musig_pubkey_xonly_tweak_add(ctx, &output_pk, &shared->keyagg_cache, taptweak) + || !secp256k1_xonly_pubkey_from_pubkey(ctx, &shared->output, NULL, &output_pk)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Round one, the group's half. Note the arguments: a share and a label. No + * message, no cosigner, nothing from anybody else. */ +static int group_round_one(const secp256k1_context *ctx, struct group *group, + struct signing_session *session) { + unsigned int k; + + /* The label is the most delicate value in the scheme, and it is the + * caller's to choose. Every participant's secret nonces are a function of + * (its seeds, this label), and the seeds never change, so whoever picks the + * label picks everyone's secrets. + * + * Here it is a fixed byte string, which is fine for an example and wrong + * for anything else. In Lightning it is the commitment number: unique, + * strictly increasing over the channel's life, and known before the + * transaction is assembled, which is what lets this round run at all, + * since the message does not exist yet. */ + step("Choosing the session label"); + memset(session->sid, 0x2c, sizeof(session->sid)); + printf("ok, "); + print_hex(session->sid, 4); + + step("Participant nonces (secp256k1_iceberg_nonce_gen)"); + for (k = 0; k < MU; k++) { + if (!secp256k1_iceberg_nonce_gen(ctx, &session->contribution[k], &group->member[k].share, NULL, session->sid)) { + return 0; + } + session->contribution_ptr[k] = &session->contribution[k]; + } + printf("ok, %d of the %d participants\n", MU, N); + + /* Verification, not addition. The contributions are points on a degree + * t-1 polynomial in the exponent; this checks that they lie on one and + * then interpolates. It is most of what the group costs, and it is what + * stops a single participant biasing the group's nonce. + * + * NULL is the group's internal aggregate, which nothing takes back. */ + step("Combining them (secp256k1_iceberg_nonce_agg)"); + if (!secp256k1_iceberg_nonce_agg(ctx, &session->group_nonce, NULL, + session->contribution_ptr, MU, N, T, &group->pubkey)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Round one, the cosigner's half. Ordinary MuSig2, and it does not know a group + * exists. Runs before, after or alongside the function above. */ +static int cosigner_round_one(const secp256k1_context *ctx, struct cosigner *cosigner, struct shared_key *shared, + struct signing_session *session, const unsigned char *msg32) { + const secp256k1_musig_pubnonce *just_the_cosigner[1]; + unsigned char secrand[32]; + unsigned char seckey[32]; + int ok; + + step("Cosigner nonce (secp256k1_musig_nonce_gen)"); + ok = fill_random(secrand, sizeof(secrand)) + && secp256k1_keypair_sec(ctx, seckey, &cosigner->keypair) + && secp256k1_musig_nonce_gen(ctx, &cosigner->secnonce, &session->cosigner_nonce, + secrand, seckey, &cosigner->pubkey, + msg32, &shared->keyagg_cache, NULL); + secure_erase(secrand, sizeof(secrand)); + secure_erase(seckey, sizeof(seckey)); + if (!ok) { + return 0; + } + + /* Round two needs the cosigners' aggregate on its own, separately from the + * one that includes the group. With one cosigner it is an aggregate of one, + * which is not a special case anywhere. */ + just_the_cosigner[0] = &session->cosigner_nonce; + if (!secp256k1_musig_nonce_agg(ctx, &session->cosigner_aggnonce, just_the_cosigner, 1)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Both halves have published. Combine them into the session everybody signs + * against, which is where the message finally enters. */ +static int open_session(const secp256k1_context *ctx, struct shared_key *shared, + struct signing_session *session, const unsigned char *msg32) { + const secp256k1_musig_pubnonce *both[2]; + secp256k1_musig_aggnonce full_aggnonce; + + step("Session (secp256k1_musig_nonce_process)"); + both[0] = &session->group_nonce; + both[1] = &session->cosigner_nonce; + if (!secp256k1_musig_nonce_agg(ctx, &full_aggnonce, both, 2) + || !secp256k1_musig_nonce_process(ctx, &session->musig_session, &full_aggnonce, + msg32, &shared->keyagg_cache, NULL)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* Forget the secret nonces, then rebuild from disk. + * + * A FROST signer has to keep a secret nonce alive across this line, and losing + * it, restoring an old backup over it, or running two copies of the signer are + * each catastrophic. An Iceberg participant keeps no secret nonce at all: its + * nonces are a function of its share and the label, both of which it is given + * again. + * + * That property is narrower than "keeps no state". + * A participant still has to remember which labels it has already answered + * under, because two answers under one label are two equations in its three + * secrets and three are enough to recover its key share. That bookkeeping is + * not secret, and it is not optional; it is simply not this library's, since + * only the group as a whole can decide which message a label belongs to. + * + * So wipe the secrets and prove that much. Skip the parse below and the next + * call is handed a wiped share, whose magic fails an ARG_CHECK, so it aborts + * through the illegal callback rather than returning 0. */ +static int reboot_participants(const secp256k1_context *ctx, struct group *group) { + unsigned int k; + + step("Wiping every participant's secret material"); + for (k = 0; k < N; k++) { + secure_erase(&group->member[k], sizeof(group->member[k])); + } + printf("ok\n"); + + step("Rebuilding (secp256k1_iceberg_share_parse)"); + for (k = 0; k < N; k++) { + if (!secp256k1_iceberg_share_parse(ctx, &group->member[k].share, + group->disk[k].share_bytes, + group->disk[k].share_len)) { + return 0; + } + memcpy(group->member[k].answered, group->disk[k].answered_bytes, 32); + } + printf("ok\n"); + + /* Note what came back besides the share: `answered`. That record is not + * secret, which is why it sits on the same disk in the clear, and it is + * not optional, which is why it is restored here rather than left at zero. + * A participant that forgets it will answer twice. */ + return 1; +} + +/* One label, one answer: the half of the rule a participant can enforce alone. + * + * The library cannot do this for you. It holds nothing between calls, so "have I + * answered under this label before?" is a question only the caller's storage can + * answer. Here that storage is one 32-byte field per participant and the rule is + * that a label must be strictly greater than the last one signed under, which is + * free when the label is a counter. It refuses a repeat outright rather than + * asking whether the repeat was harmless: a label is one signing attempt, so an + * honest retry arrives under a new label and never needs the exception. + * + * The record reaches disk here, before the caller has a share to publish. A + * crash between signing and storing is the same as never having stored, and the + * next boot answers the label again. + * + * The other half of the rule, that no two participants answer one label on + * different messages, cannot be checked here or anywhere else inside a + * participant, because it is a fact about what other people were shown. See + * doc/iceberg.md. */ +static int may_sign_under(struct participant *member, struct storage *disk, const unsigned char *sid32) { + if (memcmp(sid32, member->answered, 32) <= 0) { + return 0; + } + memcpy(member->answered, sid32, 32); + memcpy(disk->answered_bytes, member->answered, 32); + return 1; +} + +/* The group's half of round two, signed by participants first, first+1 and + * first+2. + * + * Taking `first` is not generality for its own sake. The point of the scheme is + * that any t of them will do, so main runs this twice with different people and + * compares the two signatures. + * + * Any t of the seven, and not only the five who were in round one. The last + * section of main proves that by having a participant who sat round one out + * sign anyway. What each signer does need is the whole round-one set to check + * against, which is a different count from the number of signers. */ +static int group_signs(const secp256k1_context *ctx, struct group *group, + struct shared_key *shared, struct signing_session *session, + unsigned int first, const unsigned char *msg32, + secp256k1_musig_partial_sig *group_partial_sig) { + secp256k1_iceberg_partial_sig sig_share[T]; + const secp256k1_iceberg_partial_sig *sig_share_ptr[T]; + unsigned int k; + + /* Note what this takes: the group's own round-one contributions, not an + * aggregate of them, and not the cosigners', which arrive separately and + * already aggregated. Do not read that as an optimization waiting to happen. + * The nesting coefficient is a hash of the group's aggregate nonce, so a + * coordinator free to invent that aggregate has a coefficient it can vary at + * will, and three invented aggregates under one label give three equations in + * the same three unknowns, and the third is the key. So the aggregate is + * derived here from the contributions instead. */ + step("Signature shares (secp256k1_iceberg_partial_sign)"); + for (k = 0; k < T; k++) { + if (!may_sign_under(&group->member[first + k], &group->disk[first + k], session->sid)) { + printf("refused: participant %d has already answered under this label\n", + first + k + 1); + return 0; + } + /* The signer derives the aggregate from the contributions, then checks + * the polynomial they determine against the contribution it derives for + * itself, which ties the set to this label. It need not have been one of + * the contributors. */ + if (!secp256k1_iceberg_partial_sign(ctx, &sig_share[k], &group->member[first + k].share, NULL, + session->sid, session->contribution_ptr, MU, + &group->pubkey, &shared->keyagg_cache, + msg32, &session->cosigner_aggnonce)) { + return 0; + } + sig_share_ptr[k] = &sig_share[k]; + } + printf("ok, from participants %d, %d and %d\n", first + 1, first + 2, first + 3); + + /* Optional, and the coordinator's to decide on. Skipping it costs nothing + * until a share is bad, at which point the final signature simply fails + * BIP-340 and says nothing about which of the three caused it. Checking + * costs about what producing a share costs, per share. + * + * That is with exactly t shares, which is what this collects. Hand + * secp256k1_iceberg_partial_sig_agg one more than it needs and it refuses a + * set that disagrees with itself, cheaply and without naming anybody, for + * the same reason pubkey_agg can above: past the threshold there is a spare + * point to check against. + * + * It answers "is this share bad", not "who is lying". A share that fails + * here may have been written by somebody other than the member it names, + * and the same 0 comes back if this machine has the wrong message. */ + step("Checking them (secp256k1_iceberg_partial_sig_verify)"); + for (k = 0; k < T; k++) { + if (!secp256k1_iceberg_partial_sig_verify(ctx, &sig_share[k], &group->pubshare[first + k], + session->contribution_ptr, MU, N, T, + &group->pubkey, &shared->keyagg_cache, msg32, + &session->cosigner_aggnonce)) { + printf("share from participant %d does not check out\n", first + k + 1); + return 0; + } + } + printf("ok, all %d\n", T); + + step("Combining (secp256k1_iceberg_partial_sig_agg)"); + if (!secp256k1_iceberg_partial_sig_agg(ctx, group_partial_sig, sig_share_ptr, T, N, T)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* The cosigner's half, which happens exactly once. + * + * secp256k1_musig_partial_sign consumes the secret nonce, zeroing it on + * the way out, precisely so that signing twice with it is not something a + * caller can do by accident. Which quorum the group fielded is not the + * cosigner's business and does not change its answer. */ +static int cosigner_signs(const secp256k1_context *ctx, struct cosigner *cosigner, + struct shared_key *shared, struct signing_session *session, + secp256k1_musig_partial_sig *cosigner_partial_sig) { + step("Cosigner's share (secp256k1_musig_partial_sign)"); + if (!secp256k1_musig_partial_sign(ctx, cosigner_partial_sig, &cosigner->secnonce, + &cosigner->keypair, &shared->keyagg_cache, + &session->musig_session)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* One group share plus one cosigner share makes an ordinary MuSig2 signature. */ +static int combine(const secp256k1_context *ctx, struct signing_session *session, + const secp256k1_musig_partial_sig *group_partial_sig, + const secp256k1_musig_partial_sig *cosigner_partial_sig, + unsigned char *sig64) { + const secp256k1_musig_partial_sig *both[2]; + + step("Final signature (secp256k1_musig_partial_sig_agg)"); + both[0] = group_partial_sig; + both[1] = cosigner_partial_sig; + if (!secp256k1_musig_partial_sig_agg(ctx, sig64, &session->musig_session, both, 2)) { + return 0; + } + printf("ok\n"); + return 1; +} + +/* One call that must be refused and one that must not, with the reason beside + * each. + * + * The refusal below is a load-bearing security check that looks like a bug from + * outside; the call after it is the case that looks refusable and must not + * be. */ +static int refusals_are_refused(const secp256k1_context *ctx, struct group *group, + struct shared_key *shared, struct signing_session *session, + const unsigned char *msg32) { + secp256k1_iceberg_partial_sig sig_share; + + step("Signing over contributions from another session"); + { + /* The contributions below are a real sharing, internally consistent + * and passing the degree check, but they were produced under a + * different label. Accepting them would give whoever supplied them a + * coefficient in the signing equation that this participant cannot + * check, which is worth a signature share to an adversary and nothing + * to anybody else. + * + * The signer catches it by interpolating the contributions and + * evaluating the result at its own index: the polynomial does not pass + * through the contribution it derives locally for the label it is + * signing under. */ + secp256k1_iceberg_pubnonce elsewhere[MU]; + const secp256k1_iceberg_pubnonce *elsewhere_ptrs[MU]; + unsigned char other_sid[32]; + unsigned int k; + + memcpy(other_sid, session->sid, sizeof(other_sid)); + other_sid[0] ^= 1; + for (k = 0; k < MU; k++) { + if (!secp256k1_iceberg_nonce_gen(ctx, &elsewhere[k], &group->member[k].share, + NULL, other_sid)) { + return 0; + } + elsewhere_ptrs[k] = &elsewhere[k]; + } + /* The set is a perfectly good sharing. What it is not is a sharing of + * *this* label, and only the signer can tell, because only the signer + * holds the share that says what its own contribution should have + * been. */ + if (secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[0].share, NULL, + session->sid, elsewhere_ptrs, MU, &group->pubkey, + &shared->keyagg_cache, msg32, + &session->cosigner_aggnonce)) { + printf("FAILED: it signed, and it should not have\n"); + return 0; + } + } + printf("refused, correctly\n"); + + /* The other half of that check is a thing it must NOT cost. */ + step("Signing by a member who sat round one out"); + { + /* Participants 6 and 7 were not among the 2t-1 who produced nonces. + * They can still sign, because a contribution is a function of the + * share and the label, so an absent member can derive what its + * contribution would have been and check the polynomial against it + * without ever having been one of the contributors. + * + * This is the property that lets a quorum change between the rounds, + * which is most of why the scheme works this way: keys sit in + * cold storage and members are routinely absent rather than hostile. */ + if (!secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[N - 1].share, NULL, + session->sid, session->contribution_ptr, MU, + &group->pubkey, &shared->keyagg_cache, msg32, + &session->cosigner_aggnonce)) { + printf("FAILED: it refused, and it should not have\n"); + return 0; + } + } + printf("signed, correctly\n"); + return 1; +} + +static void print_caveats(void) { + heading("WHAT THIS EXAMPLE IS NOT"); + printf(" A trusted dealer deals the shares. Real deployments want a\n"); + printf(" distributed key generation, which this module does not provide.\n"); + printf("\n"); + printf(" Everything is passed as structs in one process. The wire formats\n"); + printf(" exist: 34 bytes for a public share, 67 for a nonce contribution,\n"); + printf(" 33 for a signature share. Nothing here uses them, so the\n"); + printf(" network is the part you still have to write.\n"); + printf("\n"); + printf(" Structs in one process also satisfy constraint 2 for free. Over a\n"); + printf(" network the channel must be authenticated and a contribution taken\n"); + printf(" only from the member its index names; without that, an adversary\n"); + printf(" supplying t of the 2t-1 picks the group's nonce and every check\n"); + printf(" here still passes.\n"); + printf("\n"); + printf(" Verifying a share tells you whether it satisfies the equation,\n"); + printf(" not who is at fault: partial signatures are forgeable, so a share\n"); + printf(" that fails may have been written by somebody else.\n"); + printf("\n"); + printf(" may_sign_under above is half the rule, and the easy half: it\n"); + printf(" stops one participant answering twice. Nothing anywhere stops two\n"); + printf(" participants answering one label on different messages. Two answers\n"); + printf(" under one label are two equations in a participant's three\n"); + printf(" secrets; three recover its key share.\n"); + printf("\n"); + printf(" The label is an argument, rather than something derived from the\n"); + printf(" message, because round one has to run before the message exists.\n"); + printf(" In Lightning the nonce is fixed a round-trip before the\n"); + printf(" transaction is assembled, and the commitment number is what\n"); + printf(" Lightning supplies instead.\n"); + printf("\n"); + printf(" So half of that discipline is yours, never answering twice under\n"); + printf(" one label, and half is the group's: agree which message a label\n"); + printf(" belongs to before anyone answers. The unforgeability proof, which\n"); + printf(" is not yet peer-reviewed, assumes both. See doc/iceberg.md.\n"); +} + +int main(void) { + secp256k1_context *ctx; + struct group group; + struct cosigner cosigner; + struct shared_key shared; + struct signing_session session; + unsigned char msg[32] = "this_could_be_the_hash_of_a_msg"; + secp256k1_musig_partial_sig group_partial_sig, cosigner_partial_sig; + unsigned char sig[64], sig_from_the_others[64]; + unsigned int k; + int ok; + + ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + printf("Iceberg: a %d-of-%d group signing as one MuSig2 participant.\n\n", T, N); + printf(" round one needs %d of the %d participants; round two needs %d\n", MU, N, MU); + printf(" online again, not necessarily the same ones, and combines %d\n", T); + printf(" signature shares. See doc/iceberg.md for why those differ.\n"); + + heading("Setup: once, by a dealer who is then not needed again"); + ok = deal_shares(ctx, &group); + + if (ok) { + heading("Key aggregation: the group is now just a public key"); + ok = aggregate_keys(ctx, &group, &cosigner, &shared); + } + if (ok) { + /* The two halves are written in this order because something has to + * go first on the page. Swap the two calls and the example still + * passes, since neither needs anything the other produces, which is why + * two Iceberg groups can sign with each other. */ + heading("Round one: needs 2t-1 participants, in no particular order"); + ok = group_round_one(ctx, &group, &session) + && cosigner_round_one(ctx, &cosigner, &shared, &session, msg) + && open_session(ctx, &shared, &session, msg); + } + if (ok) { + heading("The gap: every participant forgets its secret nonces"); + ok = reboot_participants(ctx, &group); + } + if (ok) { + heading("Round two: t shares, from any members, not just round one's"); + ok = group_signs(ctx, &group, &shared, &session, 0, msg, &group_partial_sig) + && cosigner_signs(ctx, &cosigner, &shared, &session, &cosigner_partial_sig) + && combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig); + } + if (ok) { + step("Verifying (secp256k1_schnorrsig_verify)"); + ok = secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &shared.output); + if (ok) { + printf("ok\n "); + print_hex(sig, sizeof(sig)); + printf(" An ordinary BIP-340 signature. Nothing in it records a group.\n"); + } + } + if (ok) { + heading("The same signature, from different people"); + /* Participants 4, 5 and 6 this time, with no overlap with 1, 2 and 3, and + * 6 was not in round one either. Both of those matter. Disjoint, + * because nobody may answer twice under one label. The library holds + * nothing between calls and cannot detect that, so may_sign_under above + * is what refuses it. And 6 absent from round one, because a member that + * was away can still work out what its contribution would have been. + * + * Only the group signs again. The cosigner's share is reused, since it + * has one secret nonce and spending it twice would be nonce reuse. */ + ok = group_signs(ctx, &group, &shared, &session, T, msg, &group_partial_sig) + && combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig_from_the_others); + } + if (ok) { + step("Comparing the two signatures"); + ok = memcmp(sig, sig_from_the_others, sizeof(sig)) == 0; + printf(ok ? "byte-identical\n" : "the two quorums disagreed\n"); + } + if (ok) { + heading("What the module checks, and what it leaves to you"); + ok = refusals_are_refused(ctx, &group, &shared, &session, msg); + } + if (!ok) { + printf("FAILED\n"); + } + print_caveats(); + + /* Clear the secrets: a bug elsewhere that leaks memory, or an OS that swaps + * it to disk, should not find them lying around. The group's shares are the + * long-term secret here, and the cosigner's keypair and secret nonce are + * the rest. */ + for (k = 0; k < N; k++) { + secure_erase(&group.member[k], sizeof(group.member[k])); + secure_erase(&group.disk[k], sizeof(group.disk[k])); + } + secure_erase(&cosigner, sizeof(cosigner)); + secp256k1_context_destroy(ctx); + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/include/secp256k1_iceberg.h b/include/secp256k1_iceberg.h new file mode 100644 index 00000000..70678f67 --- /dev/null +++ b/include/secp256k1_iceberg.h @@ -0,0 +1,748 @@ +#ifndef SECP256K1_ICEBERG_H +#define SECP256K1_ICEBERG_H + +#include "secp256k1_musig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** This module implements Iceberg, a threshold scheme that lets a group of + * parties stand in for a single MuSig2 (BIP 327) participant. + * + * WARNING: EXPERIMENTAL. Neither the scheme nor this implementation has been + * reviewed by anyone outside the project, and should not be used to protect + * anything of value. + * + * The group produces one ordinary MuSig2 public nonce and one ordinary MuSig2 + * partial signature, so cosigners cannot tell a group is involved and need no + * changes. + * + * The order of calls, once per group and then once per signature: + * + * setup a distributed key generation produces one share each, then + * pubshare_gen and pubkey_agg give the group's public key, + * which goes into musig_pubkey_agg beside the cosigners' + * round 1 each member of the quorum runs nonce_gen; nonce_agg turns + * those into one ordinary musig_pubnonce, and from there + * upwards this is plain MuSig2 + * round 2 each member runs partial_sign, which takes the message, the + * cosigners' nonce and the round-one contributions; + * partial_sig_agg turns the results into one ordinary + * musig_partial_sig + * + * You can find an example demonstrating the iceberg module in + * examples/iceberg.c. Further notes on API usage can be found in + * doc/iceberg.md. + * + * Nonces are derived from a session label, sid32 below, rather than stored, + * so no signer holds a secret nonce between the two rounds. A label is a + * 32-byte value the caller chooses, one per signing attempt; it is public and + * it need not be random, but it must never be used twice. That is narrower + * than keeping no state at all: a member still has to remember which labels it + * has already answered under. Choosing labels is the one thing this API cannot + * do for you, so read the note on secp256k1_iceberg_partial_sign before + * writing any of this. + * + * Three things constrain how this can be used: + * + * 1. Both rounds need a quorum of 2t-1 members online, of whom up to t-1 may + * be corrupt. Since 2t-1 members have to exist, the threshold is at most + * half the group rounded up: 2-of-3 and 3-of-5 can be expressed where + * 2-of-2 and 3-of-4 cannot. Constraint 3 then rules out 2-of-3 and 3-of-5 + * as well, leaving 2-of-4 as the smallest usable group. + * + * 2. Messages between participants must travel over authenticated channels, and + * a contribution must be accepted only from the member its index names. + * + * Every check below works by outnumbering the at most t-1 corrupt members + * with t honest ones, so that count has to be real, and this library cannot + * take it for you. It can tell that an index names a member of the group and + * that no two contributions claim the same one. It cannot tell who sent + * anything, and no check on the values could: the values of both rounds lie + * on a polynomial of degree t-1, so once t of them are public, anyone can + * compute the rest. A contribution that verifies proves the value is right, + * which is a public property, not that the named member produced it. + * + * What that costs if it is skipped: an adversary supplying t of the 2t-1 + * chooses the group's nonce. It picks a polynomial through the t-1 honest + * points it has to match, which leaves a coefficient free, and computes its + * own points on that. Every check here then passes. + * + * 3. The group must agree what it is signing before anyone answers, and + * agreeing while up to t-1 members misbehave needs n >= 3t-2 members to + * exist: 4 for a threshold of 2, 7 for 3, 10 for 4. Nothing here checks + * that, because it is a property of the group rather than of the + * arithmetic. It is still the bound to size a deployment with. + */ + +/** Maximum number of participants in a group. + * + * A participant holds one 32-byte seed per (t-1)-subset it is not a member of, + * so both storage and signing cost grow as C(n-1, t-1): 2 seeds at 2-of-3, 6 at + * 3-of-5, 126 at 5-of-10. + * + * It sizes two of the opaque types below, so it is part of this library's ABI + * and not something one member of a group can change on its own. + */ +#define SECP256K1_ICEBERG_MAX_PARTICIPANTS 10 + +/** Largest number of seeds one participant can hold, C(9, 4): the maximum of + * C(n-1, t-1) over every expressible configuration, since a quorum of 2t-1 has + * to fit in the group and so t is at most (n+1)/2. + * + * This is the number that makes a group expensive: it sizes two of the opaque + * types below, and the module's largest stack frames with them. + */ +#define SECP256K1_ICEBERG_MAX_SEEDS 126 + +/** Largest buffer secp256k1_iceberg_share_serialize can need. A share of a + * particular group serializes to 4 + 32*C(n-1, t-1) bytes, which is smaller + * for every configuration except the largest; use this to size a fixed buffer + * without computing binomials. */ +#define SECP256K1_ICEBERG_SHARE_MAX_LEN (4 + 32 * SECP256K1_ICEBERG_MAX_SEEDS) + +/** Opaque data structures + * + * The layout inside these is implementation defined and not portable between + * platforms or versions. Use the serialization and parsing functions to move + * them between machines or to storage. They contain no pointers, so they can + * be copied and moved. But a copy of a share is another copy of the secret, + * and erasing one means erasing all of them. + * + * A stray pointer or an uninitialized struct is caught rather than used. + * + * Every participant has an index in 1..n, assigned when the group is dealt. It + * travels inside the share and inside every object derived from one, so the + * aggregation functions can tell contributions apart, and a contribution + * separated from its index is not usable. + * + * The index is a claim the object makes about itself. This library checks only + * that it names a member the group has: everything above n is refused, and + * within 1..n nothing here tells a genuine contribution from one an adversary + * wrote. Binding it to a member is constraint 2 above. + */ + +/** Opaque data structure holding a participant's long-term secret share. + * + * This is the material to guard and to back up, and it is not one number: it + * is a collection of 32-byte seeds, one for every group of t-1 participants + * that this participant is NOT a member of. + * + * There is no resharing here, so a compromise or a change of membership means + * dealing the group again from scratch. + * + * Guaranteed to be 4040 bytes in size: sized for the worst case the + * compile-time maximum allows, whatever n and t you actually use. Serializing + * writes only the seeds in use. + */ +typedef struct secp256k1_iceberg_share { + unsigned char data[8 + 32 * SECP256K1_ICEBERG_MAX_SEEDS]; +} secp256k1_iceberg_share; + +/** Opaque data structure holding the Lagrange weights derived from a share. + * + * Despite traveling with the share, this contains NO secret material. The + * weights depend only on participant indices and subset structure, both + * public, so nothing here needs protecting. It exists purely so that signing + * does not repeat the work, and it can be discarded and rebuilt at any time -- + * which is the only way to move one, since it has no serialized form. + */ +typedef struct secp256k1_iceberg_share_cache { + unsigned char data[8 + 32 * SECP256K1_ICEBERG_MAX_SEEDS]; +} secp256k1_iceberg_share_cache; + +/** Opaque data structure holding one participant's public key share. + * + * A commitment to the participant's key share: the point you get by + * multiplying that share by the generator, plus the index it belongs to. It + * reveals nothing about the share and is meant to be published. This is what + * the group aggregates into its public key, and what lets everyone check that + * nobody published a wrong one. + * + * Guaranteed to be 69 bytes in size. Serializes to 34 bytes. + */ +typedef struct secp256k1_iceberg_pubshare { + unsigned char data[69]; +} secp256k1_iceberg_pubshare; + +/** Derive the Lagrange weights for a share. + * + * An optimization, not a requirement. Three calls need these weights -- + * secp256k1_iceberg_pubshare_gen, secp256k1_iceberg_nonce_gen and + * secp256k1_iceberg_partial_sign, and each accepts NULL and recomputes them, + * at the cost of one scalar inversion and multiplications growing as + * C(n-1, t-1). Build one if a participant signs often enough to care. + * + * The weights depend on n, t and the participant index and on nothing else, so + * a cache is checked against those three rather than against the seeds. One + * built for a different participant, threshold or group size calls the illegal + * callback. One built for a different group of the same shape is accepted: it + * holds the same weights the share would have derived. + * + * Returns: 1 on success. A malformed share calls the illegal callback. + * Args: ctx: pointer to a context object + * Out: cache: pointer to a cache object to initialize + * In: share: the share to derive weights for + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_share_cache_create( + const secp256k1_context *ctx, + secp256k1_iceberg_share_cache *cache, + const secp256k1_iceberg_share *share +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a share. Writes 4 + 32*C(n-1, t-1) bytes. + * + * The encoding is a version byte, then n, then t, then this participant's + * index, then the seeds. Only the seeds are secret. The three before them are + * what makes the seeds interpretable (their number is C(n-1, t-1), and which + * subset each belongs to follows from n, t and the index), so a caller that + * needs any of the three reads it here, off its own buffer. + * + * On entry outlen is the size of the buffer; on return it is the number of + * bytes this share needs, whether or not they fitted. So a short buffer costs + * a second call rather than a guess, or size it at + * SECP256K1_ICEBERG_SHARE_MAX_LEN and never ask. A malformed share has no + * length to report and sets outlen to zero, so a caller looping on it stops + * rather than asking for the same buffer forever. + * + * Returns: 1 on success, 0 if the buffer is too small. A malformed share + * calls the illegal callback. + * Args: ctx: pointer to a context object + * Out: out: buffer to write to + * In/Out: outlen: buffer size in, bytes needed out + * In: share: the share to serialize + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_share_serialize( + const secp256k1_context *ctx, + unsigned char *out, + size_t *outlen, + const secp256k1_iceberg_share *share +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Parse a share. + * + * Returns: 1 on success, 0 if the input is malformed. + * Args: ctx: pointer to a context object + * Out: share: the share to initialize + * In: in: serialized share + * inlen: its length + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_share_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_share *share, + const unsigned char *in, + size_t inlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Compute this participant's public key share. + * + * Returns: 1 on success. A malformed share calls the illegal callback. + * Args: ctx: pointer to a context object (not secp256k1_context_static) + * Out: pubshare: the public key share + * In: share: this participant's share + * cache: its Lagrange weights, or NULL to recompute them + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_pubshare_gen( + const secp256k1_context *ctx, + secp256k1_iceberg_pubshare *pubshare, + const secp256k1_iceberg_share *share, + const secp256k1_iceberg_share_cache *cache +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a public key share to 34 bytes. + * + * Returns: 1, or 0 on a malformed public share, which is a caller error and + * also calls the illegal callback. + * Args: ctx: pointer to a context object + * Out: out34: pointer to a 34-byte array to write to + * In: pubshare: the public key share to serialize + */ +SECP256K1_API int secp256k1_iceberg_pubshare_serialize( + const secp256k1_context *ctx, + unsigned char *out34, + const secp256k1_iceberg_pubshare *pubshare +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse a public key share from 34 bytes. + * + * Returns: 1 on success, 0 if the input does not encode a valid share. + * Args: ctx: pointer to a context object + * Out: pubshare: the public key share to initialize + * In: in34: pointer to a 34-byte serialized share + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_pubshare_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_pubshare *pubshare, + const unsigned char *in34 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Verify public key shares and combine them into the group public key. + * + * Checks that the shares lie on a polynomial of degree at most t-1 and then + * interpolates them. A participant who published anything else raises the + * degree and is caught here, though the check proves only that the set is + * inconsistent, not which member is at fault. + * + * Soundness needs at least t honest points present to pin the true polynomial, + * which is where the quorum of 2t-1 comes from: with at most t-1 members + * corrupt, 2t-1 contributions leave t honest ones, and t points fix a + * polynomial of degree t-1. Those t honest points have to be real: this call + * refuses an index above n, and constraint 2 is the caller's half. + * + * The result is an ordinary public key. Pass it to secp256k1_musig_pubkey_agg + * alongside the cosigners' keys exactly as if it belonged to a single signer. + * + * Returns: 1 on success, 0 if there are fewer than 2t-1 shares or more than + * the group size, if the shares are inconsistent, if two carry the + * same index, or in the vanishingly unlikely case that they combine + * to the point at infinity. + * Args: ctx: pointer to a context object + * Out: group_pk: the group's public key + * In: pubshares: array of pointers to public key shares + * n_pubshares: how many + * n: the group size the shares were dealt for. A share + * carrying an index above it is rejected: no member sits + * there, so nothing could have authenticated it + * t: the threshold, at least 1 and at most (n+1)/2. An n or + * a t outside its range is a caller bug and calls the + * illegal callback + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_pubkey_agg( + const secp256k1_context *ctx, + secp256k1_pubkey *group_pk, + const secp256k1_iceberg_pubshare * const *pubshares, + size_t n_pubshares, + unsigned int n, + unsigned int t +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Opaque data structure holding one participant's nonce contribution. + * + * Guaranteed to be 133 bytes in size. Serialized and parsed with + * secp256k1_iceberg_pubnonce_serialize and secp256k1_iceberg_pubnonce_parse. + */ +typedef struct secp256k1_iceberg_pubnonce { + unsigned char data[133]; +} secp256k1_iceberg_pubnonce; + +/** Opaque data structure holding the group's combined nonce. + * + * This is the group's internal nonce pair (R1, R2'), before the nesting + * coefficient is applied. What goes up to the cosigners is an ordinary + * secp256k1_musig_pubnonce instead. + * + * It is here for an implementation checking its own arithmetic, which needs the + * first 66 bytes of the b1 preimage and can get them nowhere else: the published + * pair has already had b1 applied and does not yield them back. The coefficient + * is + * b1 = H_Iceberg/noncecoef(R1 || R2' || P), where P is the group's public key + * and each point uses the 33-byte encoding MuSig2 uses for aggregate nonces. + * The published pair is (R1, b1*R2'): only the second point is scaled, because + * the coefficient enters as b1^(i-1). + * + * No call here takes one as input, so the serializer below is the only one + * that reads it. It is also the only object you can decline to receive: + * secp256k1_iceberg_nonce_agg writes it if you want it and skips it if you + * pass NULL. + * + * Guaranteed to be 132 bytes in size. Serialized and parsed with + * secp256k1_iceberg_aggnonce_serialize and secp256k1_iceberg_aggnonce_parse. + */ +typedef struct secp256k1_iceberg_aggnonce { + unsigned char data[132]; +} secp256k1_iceberg_aggnonce; + +/** Opaque data structure holding one participant's signature share. + * + * Guaranteed to be 37 bytes in size. Serialized and parsed with + * secp256k1_iceberg_partial_sig_serialize and + * secp256k1_iceberg_partial_sig_parse. + */ +typedef struct secp256k1_iceberg_partial_sig { + unsigned char data[37]; +} secp256k1_iceberg_partial_sig; + +/** Serialize and parse the round-one and round-two objects. + * + * pubnonce 67 bytes a member's nonce contribution, index and two points + * aggnonce 66 bytes the group's internal aggregate, no index + * partial_sig 33 bytes a member's signature share, index and one scalar + * + * A contribution and a signature share carry their participant index because + * everything that consumes them is indexed; an aggregate belongs to the group + * rather than to a member and so has none, which is the whole of the 67-versus- + * 66 difference. Points use the 33-byte encoding MuSig2 uses for aggregate + * nonces, all zero for the point at infinity. The encoding has room for it, + * though secp256k1_iceberg_nonce_agg refuses to emit one. + * + * Parsing rejects an index outside 1..SECP256K1_ICEBERG_MAX_PARTICIPANTS, a + * point that is not on the curve, and a signature share that is not a scalar + * below the group order. The parsers do not know the group size, so an index + * of 9 parses in a group of 5 and is caught when something that does know n + * tries to use it. None of these is a security boundary on its own, since a + * well-formed lie passes all of them, but each turns a value that would fail + * later into one that fails here. + * + * Returns: for a parser, 1 on success and 0 if the encoding is invalid, which + * is an ordinary thing for bytes off a network to be. For a + * serializer, 1, or 0 on a malformed object, which is a caller error + * and also calls the illegal callback. + * Args: ctx: pointer to a context object + * Out/In: the object and the fixed-size buffer named in each declaration + */ +SECP256K1_API int secp256k1_iceberg_pubnonce_serialize( + const secp256k1_context *ctx, + unsigned char *out67, + const secp256k1_iceberg_pubnonce *pubnonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_pubnonce_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_pubnonce *pubnonce, + const unsigned char *in67 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API int secp256k1_iceberg_aggnonce_serialize( + const secp256k1_context *ctx, + unsigned char *out66, + const secp256k1_iceberg_aggnonce *aggnonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_aggnonce_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_aggnonce *aggnonce, + const unsigned char *in66 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API int secp256k1_iceberg_partial_sig_serialize( + const secp256k1_context *ctx, + unsigned char *out33, + const secp256k1_iceberg_partial_sig *partial_sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_partial_sig_parse( + const secp256k1_context *ctx, + secp256k1_iceberg_partial_sig *partial_sig, + const unsigned char *in33 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Derive this participant's nonce contribution for a session. + * + * Depends on the share and the session label and nothing else: no message, no + * cosigner nonce, no key aggregation cache. That is what lets this round run + * before the message exists, which is the property the whole scheme is built + * around, and it is why a member absent from this round is not shut out of the + * next one: its contribution is determined, so it can work out what it would + * have been. There is no secret nonce object to hold between the rounds -- + * round two recomputes the value from the share and the same label. + * + * Returns: 1 on success. A malformed share calls the illegal callback. + * Args: ctx: pointer to a context object (not secp256k1_context_static) + * Out: pubnonce: this participant's contribution, to publish + * In: share: this participant's share + * cache: its Lagrange weights, or NULL to recompute them + * sid32: the session label + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_nonce_gen( + const secp256k1_context *ctx, + secp256k1_iceberg_pubnonce *pubnonce, + const secp256k1_iceberg_share *share, + const secp256k1_iceberg_share_cache *cache, + const unsigned char *sid32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(5); + +/** Verify nonce contributions and combine them. + * + * Checks both nonce sharings the way key aggregation checks the key sharing, + * and for the same reason, then interpolates them into one ordinary MuSig2 + * public nonce. From that nonce upwards this is plain MuSig2. + * + * Returns: 1 on success, 0 if there are fewer than 2t-1 contributions or more + * than the group size, if they are inconsistent, if two carry the + * same index, or if the group's nonce comes out at infinity. + * Args: ctx: pointer to a context object + * Out: musig_pubnonce: the group's nonce, to publish to the cosigners + * aggnonce: the group's internal nonce, or NULL if you do not + * want it. Nothing in this API consumes one + * In: pubnonces: the contributions + * n_pubnonces: how many + * n: the group size. A contribution carrying an index + * above it is rejected + * t: the threshold, at least 1 and at most (n+1)/2. An n + * or a t outside its range is a caller bug and calls + * the illegal callback + * group_pk: the group's public key. It feeds the nesting + * coefficient, so round two must be given the same one + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_nonce_agg( + const secp256k1_context *ctx, + secp256k1_musig_pubnonce *musig_pubnonce, + secp256k1_iceberg_aggnonce *aggnonce, + const secp256k1_iceberg_pubnonce * const *pubnonces, + size_t n_pubnonces, + unsigned int n, + unsigned int t, + const secp256k1_pubkey *group_pk +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) + SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(8); + +/** Does this key aggregation cache aggregate this list, and is the group in it? + * + * Run this once, where the cache is built, and not once per session: the key + * set belongs to the channel while a session belongs to a single signing + * attempt. + * + * secp256k1_iceberg_partial_sign takes the group's public key and the outer + * cache as two separate arguments and nothing ties them together, because + * nothing can: a cache records the hash of the key list, not the list, so + * membership is not a question it can answer. Passing a cache built over some + * other set of cosigners produces a well-formed coefficient for a key that is + * not in the aggregation, a signature share that is useless, and a spent session + * label, which is the part that matters. Retrying under that label with + * the right cache is then the attack, self-inflicted. + * + * MuSig2's own partial_sign asks for no key list, because there a wrong cache + * costs a nonce rather than a label. + * + * Tweaks are fine. The list hash is fixed when the keys are aggregated and no + * tweak touches it, so a cache carrying taproot tweaks still passes. + * + * Returns: 1 if keyagg_cache aggregates exactly this list of public keys, in + * this order, and group_pk is one of them. 0 if it aggregates some + * other list, or if group_pk is not in it. A malformed cache or public + * key is a caller bug rather than an answer, and calls the illegal + * callback as it does everywhere else in this module: neither has a + * wire format, so neither can have arrived from a peer + * Args: ctx: pointer to a context object + * In: keyagg_cache: the outer MuSig2 key aggregation cache to check + * pubkeys: the keys it should have been built from, in the order + * they were passed to secp256k1_musig_pubkey_agg + * n_pubkeys: how many, at least 1 + * group_pk: the group's public key, which must be one of them + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_keyagg_check( + const secp256k1_context *ctx, + const secp256k1_musig_keyagg_cache *keyagg_cache, + const secp256k1_pubkey * const *pubkeys, + size_t n_pubkeys, + const secp256k1_pubkey *group_pk +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(5); + +/** Produce this participant's signature share. + * + * A participant makes its own three secrets alone: both nonce scalars and its + * key share follow from its share and the label, with nobody else involved. What + * it cannot make alone is the group's aggregate nonce, which is a value at zero + * and so takes t points, and that aggregate is hashed into two of the + * coefficients those secrets get multiplied by. Hence the round-one + * contributions in the argument list below. + * + * It takes the message and the cosigners' nonce for the same reason, and + * recomputes the challenge from them. It also adds the group's own nonce + * internally, so "the aggregate nonce includes my contribution" holds by + * construction rather than by assumption. + * + * The rule this function cannot enforce for you: never call it twice with the + * same sid32. Not "never with the same message": never twice, whatever else you + * change. A participant's three secrets are fixed by the label alone and + * everything else in the signing equation is a coefficient over public values, + * so two answers under one label are two equations in three unknowns and three + * are three, at which point the key share falls out by elimination. + * + * Half of that is yours: a member can refuse to answer twice if it remembers + * what it has answered under, which means durable storage that is never rolled + * back, because this library holds nothing between calls. Recording the label + * is enough, and it is all that is enough: the label is one signing attempt, + * so a retry is a new label rather than a second answer under the old one. The + * other half is the group's: two members must not answer one label on different + * messages, which no member can detect, since what the others were shown is not + * something the protocol tells it. That is constraint 3 at the top of this file. + * Where the label is a commitment number and an attempt counter, both halves + * come from machinery the surrounding protocol runs anyway, and + * examples/iceberg.c shows the member's half. + * + * What it does check: it builds the group's aggregate from the contributions + * rather than accepting one, because the nesting coefficient is a hash of that + * aggregate and a coordinator free to invent it would hold a coefficient it + * could vary at will. It then interpolates the set and compares the result at + * this participant's own index against the contribution it derives locally, + * which ties the set to the label being signed under. Otherwise a consistent + * sharing from a different session would pass. It also counts the signer among + * the honest points of constraint 2: a set assembled by somebody else has to + * agree with this participant's own share as well as with the rest. The + * participant need not be one of the contributors, since it holds the share + * that determines what its own contribution would have been either way. + * + * Returns: 1 on success, 0 if there are fewer than 2t-1 contributions or more + * than the group size, if two carry the same index, or if the set is + * inconsistent or belongs to another session. The group size is not an + * argument here: it comes off the caller's own share. A malformed + * share, or a share cache built for a different participant, calls the + * illegal callback + * Args: ctx: pointer to a context object (not + * secp256k1_context_static) + * Out: partial_sig: the signature share to publish + * In: share: this participant's share + * cache: its Lagrange weights, or NULL to recompute them + * sid32: the session label, the same one round one used + * pubnonces: the group's own round-one contributions, one per + * member. Not the cosigners', which arrive already + * aggregated as cosigner_aggnonce below. Pass every valid + * one you have; any qualifying set gives the same result, + * so members need not agree on which + * n_pubnonces: how many, at least 2t-1 and at most the group size + * group_pk: the group's public key, the one round one was given + * keyagg_cache: the outer MuSig2 key aggregation cache + * msg32: the message being signed + * cosigner_aggnonce: the cosigners' aggregate nonce, theirs alone. Not the + * other group members', and not including the group's + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_partial_sign( + const secp256k1_context *ctx, + secp256k1_iceberg_partial_sig *partial_sig, + const secp256k1_iceberg_share *share, + const secp256k1_iceberg_share_cache *cache, + const unsigned char *sid32, + const secp256k1_iceberg_pubnonce * const *pubnonces, + size_t n_pubnonces, + const secp256k1_pubkey *group_pk, + const secp256k1_musig_keyagg_cache *keyagg_cache, + const unsigned char *msg32, + const secp256k1_musig_aggnonce *cosigner_aggnonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8) + SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11); + +/** Check one signature share against what its author published. + * + * Verifies s_k*G == +-(R1,k + b0*b1*R2,k) + e*a*g*gacc*D_k, the equation + * secp256k1_iceberg_partial_sign solves. Without this, + * secp256k1_iceberg_partial_sig_agg turns one bad share into a signature that + * fails to verify with nothing to say why. + * + * The participant's nonce is read off the contributions rather than passed in. + * They determine one polynomial per nonce, and its value at that index is what + * the participant's contribution had to be, so this also works for a member + * that sat out round one and published no contribution of its own. + * + * What a 0 means, and what it does not. It means this share does not satisfy + * that equation against this pubnonce and this public share. It does not name a + * culprit. A MuSig2 partial signature is forgeable by anyone who knows the + * session's public values, so a share that fails here may have been written by + * somebody other than the participant it is attributed to. Assigning blame is a + * stronger claim and needs an honest-supermajority quorum: 3t-2 members + * online at once, rather than the 2t-1 this scheme otherwise requires. Use this + * to find out that a signing attempt will fail before spending an aggregation + * on it, and to narrow where to look. Do not use it as evidence against a + * member. + * + * A 0 also does not distinguish a bad share from bad inputs. The same answer + * comes back if the message or the cosigners' aggregate nonce differs from the + * one the signer had. The set of contributions is the exception: any qualifying + * set from the session determines the same polynomial, so it need not be the + * set the signer used. + * + * Every other argument must be the one secp256k1_iceberg_partial_sign was + * given. The public share is what names the participant, and a signature share + * carrying a different index is refused rather than verified against its own, + * which catches the two arguments being drawn from different members. + * + * Returns: 1 if the share satisfies the equation, 0 otherwise, including a + * contribution count outside 2t-1..n, and including a malformed + * partial_sig, which is the object being examined. A malformed + * pubshare, key aggregation cache, group key or cosigner aggregate is + * the caller's own and calls the illegal callback + * Args: ctx: pointer to a context object + * In: partial_sig: the signature share to check + * pubshare: the public share of the participant it is + * attributed to, which is what says which one that is + * pubnonces: a qualifying set from the same session. It need not + * be the set the signer used: any consistent 2t-1 + * determine the same polynomial + * n_pubnonces: how many, at least 2t-1 and at most n + * n: the group size. A contribution carrying an index + * above it is rejected + * t: the threshold, at least 1 and at most (n+1)/2. An + * n or a t outside its range is a caller bug and + * calls the illegal callback + * group_pk: the group's aggregate public key + * keyagg_cache: the MuSig2 cache for the outer session + * msg32: the message being signed + * cosigner_aggnonce: the cosigners' aggregate nonce + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_partial_sig_verify( + const secp256k1_context *ctx, + const secp256k1_iceberg_partial_sig *partial_sig, + const secp256k1_iceberg_pubshare *pubshare, + const secp256k1_iceberg_pubnonce * const *pubnonces, + size_t n_pubnonces, + unsigned int n, + unsigned int t, + const secp256k1_pubkey *group_pk, + const secp256k1_musig_keyagg_cache *keyagg_cache, + const unsigned char *msg32, + const secp256k1_musig_aggnonce *cosigner_aggnonce +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9) + SECP256K1_ARG_NONNULL(10) SECP256K1_ARG_NONNULL(11); + +/** Combine signature shares into one MuSig2 partial signature. + * + * Interpolation needs t points and the quorum bound exists only for the degree + * check, so unlike the nonce round this takes t rather than 2t-1. The scheme + * as specified keeps 2t-1 online throughout regardless; this call simply does + * not need them all to have answered. + * + * The result is an ordinary MuSig2 partial signature. Pass it to + * secp256k1_musig_partial_sig_agg with the cosigners' partial signatures. + * + * Given more than t shares, this refuses a set that contradicts itself. The + * shares of one session lie on a polynomial of degree t-1, as the contributions + * of the nonce round do, so anything past the t needed to interpolate is a + * second opinion and is treated as one. Passing a spare share is therefore + * worth something, and can turn a call that would have succeeded into one that + * fails: at exactly t there is nothing to disagree with, and a bad share is + * aggregated into a signature that fails later without naming a share. + * + * This is not verification. It says the shares agree with each other, not that + * they are the ones the members would have produced, and it cannot tell you + * which of them is the odd one out. secp256k1_iceberg_partial_sig_verify + * answers both, one share at a time, against the public share it names. + * + * Returns: 1 on success, 0 if there are fewer than t shares or more than the + * group size, if a share carries an out-of-range index, if two carry + * the same one, or if more than t shares are given and they do not + * lie on one polynomial of degree t-1. + * Args: ctx: pointer to a context object + * Out: musig_partial_sig: the group's partial signature + * In: partial_sigs: the signature shares. An uninitialized one calls + * the illegal callback; + * a share off the wire is fine, since + * secp256k1_iceberg_partial_sig_parse writes the + * same tag secp256k1_iceberg_partial_sign does + * n_partial_sigs: how many + * n: the group size. A share carrying an index above + * it is rejected + * t: the threshold, at least 1 and at most (n+1)/2. + * An n or a t outside its range is a caller bug + * and calls the illegal callback + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_partial_sig_agg( + const secp256k1_context *ctx, + secp256k1_musig_partial_sig *musig_partial_sig, + const secp256k1_iceberg_partial_sig * const *partial_sigs, + size_t n_partial_sigs, + unsigned int n, + unsigned int t +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_ICEBERG_H */ diff --git a/include/secp256k1_iceberg_dealer.h b/include/secp256k1_iceberg_dealer.h new file mode 100644 index 00000000..d54b48e9 --- /dev/null +++ b/include/secp256k1_iceberg_dealer.h @@ -0,0 +1,57 @@ +#ifndef SECP256K1_ICEBERG_DEALER_H +#define SECP256K1_ICEBERG_DEALER_H + +#include "secp256k1_iceberg.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** A trusted dealer for Iceberg shares. + * + * This header is deliberately separate from secp256k1_iceberg.h and is not + * installed. Including the module's own header does not offer you a dealer, + * because for the duration of the call below one machine holds everything + * needed to reconstruct the group's private key, which is the situation a + * threshold scheme exists to avoid. + * + * It is here so that the tests, the benchmarks and the example have shares to + * work with. It is also usable where one party is already trusted with the + * whole key, which is a real if narrow case. Anything else wants a distributed + * key generation: the same shares, assembled without the key ever existing in + * one place. This module does not provide one. + * + * Nothing in the signing API depends on how a share was produced. A share + * arrives through secp256k1_iceberg_share_parse, so an externally generated + * one, from a DKG or from another implementation, is used exactly the same + * way. + */ + +/** Deal a group's shares from a single seed. + * + * The seed is the group's private key in all but name until this returns. + * Erase it afterwards, and do not derive it from anything reproducible. + * + * Returns: 1 on success. + * Args: ctx: pointer to a context object + * Out: shares: array of n pointers to share objects, participant k at k-1 + * In: n: number of participants, 1 to + * SECP256K1_ICEBERG_MAX_PARTICIPANTS + * t: threshold, at least 1 and at most (n+1)/2, since a quorum of + * 2t-1 has to fit in the group. So 2-of-2 and 3-of-4 are + * inexpressible + * seed32: 32 bytes of uniformly random data + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_iceberg_shares_gen( + const secp256k1_context *ctx, + secp256k1_iceberg_share * const *shares, + unsigned int n, + unsigned int t, + const unsigned char *seed32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(5); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_ICEBERG_DEALER_H */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9a66f38f..f84bb377 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,15 @@ if(SECP256K1_ENABLE_MODULE_ELLSWIFT) set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_ellswift.h) endif() +if(SECP256K1_ENABLE_MODULE_ICEBERG) + if(DEFINED SECP256K1_ENABLE_MODULE_MUSIG AND NOT SECP256K1_ENABLE_MODULE_MUSIG) + message(FATAL_ERROR "Module dependency error: You have disabled the musig module explicitly, but it is required by the iceberg module.") + endif() + set(SECP256K1_ENABLE_MODULE_MUSIG ON) + add_compile_definitions(ENABLE_MODULE_ICEBERG=1) + set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_iceberg.h) +endif() + if(SECP256K1_ENABLE_MODULE_MUSIG) if(DEFINED SECP256K1_ENABLE_MODULE_SCHNORRSIG AND NOT SECP256K1_ENABLE_MODULE_SCHNORRSIG) message(FATAL_ERROR "Module dependency error: You have disabled the schnorrsig module explicitly, but it is required by the musig module.") @@ -191,6 +200,10 @@ if(SECP256K1_BUILD_BENCHMARK) target_link_libraries(bench_internal secp256k1_precomputed secp256k1_asm) add_executable(bench_ecmult bench_ecmult.c) target_link_libraries(bench_ecmult secp256k1_precomputed secp256k1_asm) + if(SECP256K1_ENABLE_MODULE_ICEBERG) + add_executable(bench_iceberg bench_iceberg.c) + target_link_libraries(bench_iceberg secp256k1_precomputed secp256k1_asm) + endif() endif() if(SECP256K1_BUILD_TESTS) diff --git a/src/bench_iceberg.c b/src/bench_iceberg.c new file mode 100644 index 00000000..24828109 --- /dev/null +++ b/src/bench_iceberg.c @@ -0,0 +1,97 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#include +#include +#include + +#include "secp256k1.c" +#include "../include/secp256k1.h" +#include "util.h" +#include "bench.h" +#include "modules/iceberg/bench_impl.h" + +/* Small sizes, spread out enough to show the growth curve. These are not + * deployment recommendations: all five are below the n >= 3t-2 that the + * agreement around the scheme needs, and 4-of-7 versus 5-of-9 is here because + * the seed count roughly triples between them, not because anyone runs it. */ +static const unsigned int CONFIGS[][2] = { {3,2}, {5,3}, {7,4}, {9,5}, {10,5} }; + +int main(int argc, char **argv) { + bench_iceberg_data data; + int iters = get_iters(1000); + size_t c; + + (void)argc; (void)argv; + data.ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + printf("%-22s", ""); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + char label[16]; + sprintf(label, "%u-of-%u", CONFIGS[c][1], CONFIGS[c][0]); + printf("%12s", label); + } + printf("\n"); + printf("%-22s", "seeds per participant"); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + printf("%12u", secp256k1_rss_binom(CONFIGS[c][0] - 1, CONFIGS[c][1] - 1)); + } + printf("\n\n"); + + { + /* per_sig marks the rows a signing session actually pays for, so the + * total at the bottom comes from the same run as the rows above it + * rather than from someone adding them up by hand. */ + struct { const char *name; void (*fn)(void*, int); int per_sig; const char *note; } benches[] = { + { "shares_gen", bench_iceberg_shares_gen, 0, " (setup only)" }, + { "share_cache_create",bench_iceberg_cache_create, 0, " (setup only)" }, + { "pubshare_gen", bench_iceberg_pubshare_gen, 0, " (setup only)" }, + { "pubkey_agg", bench_iceberg_pubkey_agg, 0, " (setup only)" }, + { "nonce_gen", bench_iceberg_nonce_gen, 1, "" }, + { "nonce_agg", bench_iceberg_nonce_agg, 1, "" }, + { "partial_sign", bench_iceberg_partial_sign, 1, "" }, + { "partial_sig_agg", bench_iceberg_partial_sig_agg, 1, "" }, + { "partial_sig_verify",bench_iceberg_partial_sig_verify, 0, " (optional, per share)" }, + { " degree_check", bench_iceberg_degree_check, 0, " (inside nonce_agg, partial_sign and partial_sig_verify)" }, + { " interpolate", bench_iceberg_interpolate, 0, " (inside nonce_agg, partial_sign and partial_sig_verify)" }, + { " lagrange_basis", bench_iceberg_lagrange_basis, 0, " (inside degree_check)" } + }; + double per_signature[sizeof(CONFIGS)/sizeof(CONFIGS[0])] = { 0 }; + size_t b; + for (b = 0; b < sizeof(benches)/sizeof(benches[0]); b++) { + printf("%-22s", benches[b].name); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + int64_t begin, total; + double each; + int i; + data.n = CONFIGS[c][0]; + data.t = CONFIGS[c][1]; + data.mu = 2 * data.t - 1; + bench_iceberg_setup(&data); + benches[b].fn(&data, 2); /* warm up */ + begin = gettime_i64(); + for (i = 0; i < iters; i++) { + benches[b].fn(&data, 1); + } + total = gettime_i64() - begin; + each = (double)total / iters; + if (benches[b].per_sig) { + per_signature[c] += each; + } + printf("%9.1f us", each); + } + printf("%s\n", benches[b].note); + } + + printf("\n%-22s", "per signature"); + for (c = 0; c < sizeof(CONFIGS)/sizeof(CONFIGS[0]); c++) { + printf("%9.1f us", per_signature[c]); + } + printf("\n"); + } + + secp256k1_context_destroy(data.ctx); + return EXIT_SUCCESS; +} diff --git a/src/ctime_tests.c b/src/ctime_tests.c index a47b0ffa..9635ee77 100644 --- a/src/ctime_tests.c +++ b/src/ctime_tests.c @@ -61,6 +61,11 @@ #include "../include/secp256k1_chilldkg.h" #endif +#ifdef ENABLE_MODULE_ICEBERG +#include "../include/secp256k1_iceberg.h" +#include "../include/secp256k1_iceberg_dealer.h" +#endif + static void run_tests(secp256k1_context *ctx, unsigned char *key); int main(void) { @@ -587,6 +592,119 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) { SECP256K1_CHECKMEM_DEFINE(ack_sig, sizeof(ack_sig)); } #endif + +#ifdef ENABLE_MODULE_ICEBERG + { + /* A 3-of-5 group, dealt from `key` and taken as far as one signature + * share. Secret here is the dealer's root seed and everything the + * module derives from it: the per-subset seeds, the key share, and the + * two nonce shares. Not secret: participant indices, the group and + * threshold, every Lagrange weight, the commitments, the session label, + * and both nonce coefficients. */ + enum { ICEBERG_N = 5, ICEBERG_T = 3, ICEBERG_MU = 2 * ICEBERG_T - 1 }; + secp256k1_iceberg_share shares[ICEBERG_N]; + secp256k1_iceberg_share *share_ptr[ICEBERG_N]; + secp256k1_iceberg_share_cache share_cache; + secp256k1_iceberg_pubshare pubshares[ICEBERG_N]; + const secp256k1_iceberg_pubshare *pubshare_ptr[ICEBERG_N]; + secp256k1_iceberg_pubnonce nonces[ICEBERG_N]; + const secp256k1_iceberg_pubnonce *nonce_ptr[ICEBERG_N]; + secp256k1_iceberg_aggnonce iceberg_aggnonce; + secp256k1_iceberg_partial_sig iceberg_psig; + secp256k1_musig_pubnonce group_pubnonce, cosigner_pubnonce; + const secp256k1_musig_pubnonce *cosigner_ptr[1]; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_keyagg_cache iceberg_cache; + secp256k1_pubkey group_pk, cosigner_pk; + const secp256k1_pubkey *iceberg_pk_ptr[2]; + unsigned char share_bytes[SECP256K1_ICEBERG_SHARE_MAX_LEN]; + unsigned char sid[32], cosigner_secrand[32]; + size_t share_len; + int party; + + for (party = 0; party < ICEBERG_N; party++) { + share_ptr[party] = &shares[party]; + pubshare_ptr[party] = &pubshares[party]; + nonce_ptr[party] = &nonces[party]; + } + SECP256K1_CHECKMEM_DEFINE(key, 32); + /* The cosigner needs randomness distinct from the dealer's root seed; + * any perturbation of `key` will do. */ + memcpy(cosigner_secrand, key, sizeof(cosigner_secrand)); + cosigner_secrand[0] = cosigner_secrand[0] + 3; + CHECK(secp256k1_keypair_create(ctx, &keypair, key)); + CHECK(secp256k1_keypair_pub(ctx, &cosigner_pk, &keypair)); + + SECP256K1_CHECKMEM_UNDEFINE(key, 32); + ret = secp256k1_iceberg_shares_gen(ctx, share_ptr, ICEBERG_N, ICEBERG_T, key); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + + /* Storing and restoring a share moves seed material through a buffer, + * which is where a length or an offset computed from it would show. */ + share_len = sizeof(share_bytes); + ret = secp256k1_iceberg_share_serialize(ctx, share_bytes, &share_len, &shares[0]); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + ret = secp256k1_iceberg_share_parse(ctx, &shares[0], share_bytes, share_len); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + + ret = secp256k1_iceberg_share_cache_create(ctx, &share_cache, &shares[0]); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + /* The header says the cache holds no secret, and this is the line that + * makes that a result rather than a claim. Every byte of it must be + * defined: the weights come from the group size, the threshold and the + * participant index, all of which are public, and none of them from a + * seed. Declassifying is not the same test: it would say the value may be + * published, where this says nothing secret reached it. */ + SECP256K1_CHECKMEM_CHECK(&share_cache, sizeof(share_cache)); + + for (party = 0; party < ICEBERG_N; party++) { + ret = secp256k1_iceberg_pubshare_gen(ctx, &pubshares[party], &shares[party], + party == 0 ? &share_cache : NULL); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } + + /* A commitment is public, and so is everything built from one. */ + SECP256K1_CHECKMEM_DEFINE(pubshares, sizeof(pubshares)); + CHECK(secp256k1_iceberg_pubkey_agg(ctx, &group_pk, pubshare_ptr, ICEBERG_MU, ICEBERG_N, ICEBERG_T) == 1); + + iceberg_pk_ptr[0] = &group_pk; + iceberg_pk_ptr[1] = &cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(ctx, NULL, &iceberg_cache, iceberg_pk_ptr, 2)); + SECP256K1_CHECKMEM_DEFINE(msg, sizeof(msg)); + SECP256K1_CHECKMEM_UNDEFINE(cosigner_secrand, sizeof(cosigner_secrand)); + ret = secp256k1_musig_nonce_gen(ctx, &cosigner_secnonce, &cosigner_pubnonce, + cosigner_secrand, NULL, &cosigner_pk, msg, + &iceberg_cache, NULL); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + SECP256K1_CHECKMEM_DEFINE(&cosigner_pubnonce, sizeof(cosigner_pubnonce)); + cosigner_ptr[0] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(ctx, &cosigner_aggnonce, cosigner_ptr, 1)); + memset(sid, 0x7e, sizeof(sid)); /* public: the label is the caller's to choose */ + + for (party = 0; party < ICEBERG_MU; party++) { + ret = secp256k1_iceberg_nonce_gen(ctx, &nonces[party], &shares[party], + party == 0 ? &share_cache : NULL, sid); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } + SECP256K1_CHECKMEM_DEFINE(nonces, sizeof(nonces)); + CHECK(secp256k1_iceberg_nonce_agg(ctx, &group_pubnonce, &iceberg_aggnonce, + nonce_ptr, ICEBERG_MU, ICEBERG_N, ICEBERG_T, &group_pk) == 1); + + ret = secp256k1_iceberg_partial_sign(ctx, &iceberg_psig, &shares[0], &share_cache, + sid, nonce_ptr, ICEBERG_MU, &group_pk, + &iceberg_cache, msg, &cosigner_aggnonce); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } +#endif } #if defined(__GNUC__) diff --git a/src/modules/iceberg/Makefile.am.include b/src/modules/iceberg/Makefile.am.include new file mode 100644 index 00000000..a653aee3 --- /dev/null +++ b/src/modules/iceberg/Makefile.am.include @@ -0,0 +1,16 @@ +include_HEADERS += include/secp256k1_iceberg.h +# In the tree so tests, benchmarks and the example can deal shares; not +# installed, because a trusted dealer is not part of what this library offers. +noinst_HEADERS += include/secp256k1_iceberg_dealer.h +noinst_HEADERS += src/modules/iceberg/main_impl.h +noinst_HEADERS += src/modules/iceberg/bench_impl.h +noinst_HEADERS += src/modules/iceberg/scalar_poly.h +noinst_HEADERS += src/modules/iceberg/scalar_poly_impl.h +noinst_HEADERS += src/modules/iceberg/rss.h +noinst_HEADERS += src/modules/iceberg/rss_impl.h +noinst_HEADERS += src/modules/iceberg/vpss.h +noinst_HEADERS += src/modules/iceberg/vpss_impl.h +noinst_HEADERS += src/modules/iceberg/keygen_impl.h +noinst_HEADERS += src/modules/iceberg/session_impl.h +noinst_HEADERS += src/modules/iceberg/tests_impl.h +noinst_HEADERS += src/modules/iceberg/vectors.h diff --git a/src/modules/iceberg/bench_impl.h b/src/modules/iceberg/bench_impl.h new file mode 100644 index 00000000..6b380958 --- /dev/null +++ b/src/modules/iceberg/bench_impl.h @@ -0,0 +1,221 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_BENCH_H +#define SECP256K1_MODULE_ICEBERG_BENCH_H + +#include + +#include "../../../include/secp256k1_iceberg.h" +#include "../../../include/secp256k1_iceberg_dealer.h" + +/* Timings for each step of a signing session. + * + * Two growth laws meet here, which is what the configurations are chosen to + * show rather than any of them being deployable; see doc/iceberg.md. Each + * participant holds C(n-1, t-1) seeds (2 at 2-of-3, 126 at 5-of-10) and + * hashes every one on every derivation, while the curve work grows only with the + * quorum 2t-1, since that is the width of the multiexponentiation in the degree + * check and everything else on the curve is a fixed number of multiplications. + * The two grow at different rates. */ + +typedef struct { + secp256k1_context *ctx; + unsigned int n, t, mu; + + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share_cache cache; + secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubnonce nonces[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubnonce *nonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_partial_sig psigs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_partial_sig *psig_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_aggnonce aggnonce; + + secp256k1_pubkey group_pk, cosigner_pk; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_partial_sig group_psig; + + unsigned char seed[32], msg[32], sid[32]; + + /* One of the two nonce sharings, kept in the form the group layer wants it, + * so the degree check and the interpolation can be timed on their own. */ + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge commitments[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; +} bench_iceberg_data; + +static void bench_iceberg_setup(bench_iceberg_data *d) { + const secp256k1_pubkey *pubkeys[2]; + const secp256k1_musig_pubnonce *one[1]; + unsigned char seckey[32], secrand[32]; + unsigned int k; + + memset(d->seed, 0x2a, 32); + memset(d->msg, 0x5c, 32); + memset(seckey, 0x31, 32); + memset(secrand, 0x77, 32); + + for (k = 1; k <= d->n; k++) { + d->share_ptrs[k - 1] = &d->shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(d->ctx, d->share_ptrs, d->n, d->t, d->seed)); + CHECK(secp256k1_iceberg_share_cache_create(d->ctx, &d->cache, &d->shares[0])); + for (k = 1; k <= d->n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(d->ctx, &d->pubshares[k - 1], &d->shares[k - 1], NULL)); + d->pubshare_ptrs[k - 1] = &d->pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(d->ctx, &d->group_pk, d->pubshare_ptrs, d->mu, d->n, d->t)); + + CHECK(secp256k1_keypair_create(d->ctx, &d->cosigner_keypair, seckey)); + CHECK(secp256k1_keypair_pub(d->ctx, &d->cosigner_pk, &d->cosigner_keypair)); + pubkeys[0] = &d->group_pk; + pubkeys[1] = &d->cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(d->ctx, &d->agg_xonly, &d->keyagg_cache, pubkeys, 2)); + + CHECK(secp256k1_musig_nonce_gen(d->ctx, &d->cosigner_secnonce, &d->cosigner_pubnonce, + secrand, seckey, &d->cosigner_pk, d->msg, &d->keyagg_cache, NULL)); + one[0] = &d->cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(d->ctx, &d->cosigner_aggnonce, one, 1)); + memset(d->sid, 0x2a, sizeof(d->sid)); + + for (k = 1; k <= d->n; k++) { + CHECK(secp256k1_iceberg_nonce_gen(d->ctx, &d->nonces[k - 1], &d->shares[k - 1], NULL, d->sid)); + d->nonce_ptrs[k - 1] = &d->nonces[k - 1]; + } + CHECK(secp256k1_iceberg_nonce_agg(d->ctx, &d->group_pubnonce, &d->aggnonce, + d->nonce_ptrs, d->mu, d->n, d->t, &d->group_pk)); + for (k = 1; k <= d->mu; k++) { + secp256k1_ge pts[2]; + unsigned int who; + CHECK(secp256k1_iceberg_pubnonce_load(d->ctx, &who, pts, &d->nonces[k - 1])); + d->idx[k - 1] = (unsigned char)who; + d->commitments[k - 1] = pts[0]; + } + for (k = 1; k <= d->t; k++) { + /* A cache is bound to one participant, so pass NULL and let each of + * these derive its own. */ + CHECK(secp256k1_iceberg_partial_sign(d->ctx, &d->psigs[k - 1], &d->shares[k - 1], NULL, d->sid, d->nonce_ptrs, d->mu, &d->group_pk, &d->keyagg_cache, d->msg, &d->cosigner_aggnonce)); + d->psig_ptrs[k - 1] = &d->psigs[k - 1]; + } +} + +static void bench_iceberg_shares_gen(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_shares_gen(d->ctx, d->share_ptrs, d->n, d->t, d->seed)); + } +} + +static void bench_iceberg_cache_create(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_share_cache_create(d->ctx, &d->cache, &d->shares[0])); + } +} + +static void bench_iceberg_pubshare_gen(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_pubshare_gen(d->ctx, &d->pubshares[0], &d->shares[0], &d->cache)); + } +} + +static void bench_iceberg_pubkey_agg(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_pubkey_agg(d->ctx, &d->group_pk, d->pubshare_ptrs, d->mu, d->n, d->t)); + } +} + +static void bench_iceberg_nonce_gen(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_nonce_gen(d->ctx, &d->nonces[0], &d->shares[0], &d->cache, d->sid)); + } +} + +static void bench_iceberg_nonce_agg(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_nonce_agg(d->ctx, &d->group_pubnonce, &d->aggnonce, + d->nonce_ptrs, d->mu, d->n, d->t, &d->group_pk)); + } +} + +static void bench_iceberg_partial_sign(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_partial_sign(d->ctx, &d->psigs[0], &d->shares[0], &d->cache, d->sid, d->nonce_ptrs, d->mu, &d->group_pk, &d->keyagg_cache, d->msg, &d->cosigner_aggnonce)); + } +} + +/* Optional, and priced per share rather than per signature: a caller that wants + * to know which share is bad runs this t times before aggregating once. */ +static void bench_iceberg_partial_sig_verify(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_partial_sig_verify(d->ctx, &d->psigs[0], &d->pubshares[0], d->nonce_ptrs, d->mu, d->n, d->t, &d->group_pk, &d->keyagg_cache, d->msg, &d->cosigner_aggnonce)); + } +} + +/* The degree check and the interpolation, timed on their own. Both run once per + * nonce sharing inside nonce_agg, again inside partial_sign and inside + * partial_sig_verify, and once more in pubkey_agg at setup. + * Nothing in the API reaches these directly; they are here to take the rows + * above apart. */ +static void bench_iceberg_degree_check(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_vpss_verify_var(d->ctx, d->idx, d->commitments, d->mu, d->t)); + } +} + +static void bench_iceberg_interpolate(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + secp256k1_gej r; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_vpss_combine_var(d->ctx, &r, d->idx, d->commitments, d->mu)); + } +} + +/* And inside the degree check, the m x m basis, which depends on nothing but + * the participant indices. All four checks in a signing session build the same + * one. */ +static void bench_iceberg_lagrange_basis(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + secp256k1_scalar basis[SECP256K1_ICEBERG_MAX_PARTICIPANTS * SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + int i; + for (i = 0; i < iters; i++) { + secp256k1_scalarpoly_lagrange_basis_var(basis, d->idx, d->mu); + } +} + +static void bench_iceberg_partial_sig_agg(void *arg, int iters) { + bench_iceberg_data *d = (bench_iceberg_data *)arg; + int i; + for (i = 0; i < iters; i++) { + CHECK(secp256k1_iceberg_partial_sig_agg(d->ctx, &d->group_psig, d->psig_ptrs, + d->t, d->n, d->t)); + } +} + +#endif /* SECP256K1_MODULE_ICEBERG_BENCH_H */ diff --git a/src/modules/iceberg/keygen_impl.h b/src/modules/iceberg/keygen_impl.h new file mode 100644 index 00000000..9467cad6 --- /dev/null +++ b/src/modules/iceberg/keygen_impl.h @@ -0,0 +1,422 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_KEYGEN_IMPL_H +#define SECP256K1_MODULE_ICEBERG_KEYGEN_IMPL_H + +#include + +#include "../../../include/secp256k1_iceberg.h" +#include "../../../include/secp256k1_iceberg_dealer.h" + +#include "rss_impl.h" +#include "scalar_poly_impl.h" +#include "vpss_impl.h" + +#include "../../group.h" +#include "../../hash.h" +#include "../../scalar.h" +#include "../../util.h" + +/* The label under which the signing key itself is derived. Everything else the + * group produces is keyed on a session label; the key is keyed on this fixed + * string, so it is the one sharing that never changes. Fourteen bytes, matching + * the reference implementation; shares would not be portable otherwise. */ +static const unsigned char secp256k1_iceberg_keygen_label[14] = { + 'I', 'c', 'e', 'b', 'e', 'r', 'g', '/', 'k', 'e', 'y', 'g', 'e', 'n' +}; + +static const unsigned char secp256k1_iceberg_share_magic[4] = { 0x1c, 0xeb, 0x27, 0x5a }; +static const unsigned char secp256k1_iceberg_cache_magic[4] = { 0x1c, 0xeb, 0xc4, 0x03 }; +static const unsigned char secp256k1_iceberg_pubshare_magic[4] = { 0x1c, 0xeb, 0x9d, 0xf1 }; + +/* share: magic(4) | n(1) t(1) k(1) pad(1) | seeds[MAX_SEEDS][32] + * cache: magic(4) | n(1) t(1) k(1) pad(1) | weights[MAX_SEEDS][32] + * pubshare: magic(4) | index(1) | ge_to_bytes_ext(64) + * + * Both share and cache are sized for the worst configuration, not the one in + * use, so a 2-of-3 group carries mostly padding. That is the price of a + * fixed-size opaque type in a library that never allocates; the serialized + * forms are exact. */ + +static void secp256k1_iceberg_share_save(secp256k1_iceberg_share *share, unsigned int n, unsigned int t, unsigned int k) { + /* Zero the whole object, not just the header. Only C(n-1, t-1) of the seed + * slots are ever written, and leaving the rest as whatever was on the stack + * would make two shares holding identical secrets compare unequal, and + * would put uninitialized memory into anything that copies the struct. */ + memset(share->data, 0, sizeof(share->data)); + memcpy(share->data, secp256k1_iceberg_share_magic, 4); + share->data[4] = (unsigned char)n; + share->data[5] = (unsigned char)t; + share->data[6] = (unsigned char)k; + share->data[7] = 0; +} + +/* Unpacks the header and hands back a pointer to the seeds. Returns the number + * of seeds, or 0 if the object is not a well-formed share. */ +static size_t secp256k1_iceberg_share_load(const secp256k1_context *ctx, unsigned int *n, unsigned int *t, unsigned int *k, const unsigned char **seeds, const secp256k1_iceberg_share *share) { + ARG_CHECK(secp256k1_memcmp_var(share->data, secp256k1_iceberg_share_magic, 4) == 0); + *n = share->data[4]; + *t = share->data[5]; + *k = share->data[6]; + ARG_CHECK(*n >= 1 && *n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + ARG_CHECK(*t >= 1 && *t <= (*n + 1) / 2); + ARG_CHECK(*k >= 1 && *k <= *n); + *seeds = &share->data[8]; + return secp256k1_rss_binom(*n - 1, *t - 1); +} + +/* TODO: this call is optional and every caller of it can pass NULL instead, so + * the saving has to justify an extra type in the API. What it avoids per call + * is C(n, t-1) subset unrankings and count*(2t+2) - 3 scalar multiplications, + * about 1500 at 5-of-10. bench_iceberg times share_cache_create and times the + * three consumers without a cache; it does not time them with one, so the + * saving is not yet a row you can read off. */ +int secp256k1_iceberg_share_cache_create(const secp256k1_context *ctx, secp256k1_iceberg_share_cache *cache, const secp256k1_iceberg_share *share) { + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS]; + const unsigned char *seeds; + unsigned int n, t, k; + size_t count, i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(cache != NULL); + memset(cache, 0, sizeof(*cache)); + ARG_CHECK(share != NULL); + + count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share); + if (count == 0) { + return 0; + } + if (secp256k1_rss_lagrange_weights_var(weights, n, t, k) != count) { + return 0; + } + + memcpy(cache->data, secp256k1_iceberg_cache_magic, 4); + cache->data[4] = (unsigned char)n; + cache->data[5] = (unsigned char)t; + cache->data[6] = (unsigned char)k; + cache->data[7] = 0; + for (i = 0; i < count; i++) { + secp256k1_scalar_get_b32(&cache->data[8 + 32 * i], &weights[i]); + } + return 1; +} + +/* Loads weights for this share, deriving them if the caller did not supply a + * cache. */ +static int secp256k1_iceberg_weights_for(const secp256k1_context *ctx, secp256k1_scalar *weights, size_t count, unsigned int n, unsigned int t, unsigned int k, const secp256k1_iceberg_share_cache *cache) { + size_t i; + + if (cache == NULL) { + return secp256k1_rss_lagrange_weights_var(weights, n, t, k) == count; + } + ARG_CHECK(secp256k1_memcmp_var(cache->data, secp256k1_iceberg_cache_magic, 4) == 0); + /* A cache built for a different participant would silently produce a wrong + * share, so the pairing is checked and not assumed. */ + ARG_CHECK(cache->data[4] == n && cache->data[5] == t && cache->data[6] == k); + for (i = 0; i < count; i++) { + secp256k1_scalar_set_b32(&weights[i], &cache->data[8 + 32 * i], NULL); + } + return 1; +} + +/* Initializes SHA256 with fixed midstate. This midstate was computed by applying + * SHA256 to SHA256("Iceberg/dealer")||SHA256("Iceberg/dealer"). */ +static void secp256k1_iceberg_dealer_sha256_tagged(secp256k1_sha256 *sha) { + static const uint32_t midstate[8] = { + 0xb40815eaul, 0x9e117bfaul, 0x4a71724ful, 0x1f71a00eul, + 0x19cf3ed1ul, 0xd5ff1efcul, 0xeb8b1dd5ul, 0x024d39e8ul + }; + secp256k1_sha256_initialize_midstate(sha, 64, midstate); +} + +/* The seed named after one subset: H_"Iceberg/dealer"(root || n || t || rank). + * + * It depends on the rank and not on who receives it, which is the whole of + * replicated sharing: every participant outside the subset is handed this same + * value. Deriving all of them from one root keeps dealing reproducible, which is + * what makes test vectors possible, and it also means the root is as sensitive + * as the group key and must be destroyed afterwards. */ +static void secp256k1_iceberg_dealer_seed(const secp256k1_context *ctx, unsigned char *seed, const unsigned char *root32, unsigned int n, unsigned int t, uint32_t rank) { + secp256k1_sha256 sha; + unsigned char header[6]; + + header[0] = (unsigned char)n; + header[1] = (unsigned char)t; + header[2] = (unsigned char)(rank >> 24); + header[3] = (unsigned char)(rank >> 16); + header[4] = (unsigned char)(rank >> 8); + header[5] = (unsigned char)rank; + secp256k1_iceberg_dealer_sha256_tagged(&sha); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, root32, 32); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, header, sizeof(header)); + secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &sha, seed); + secp256k1_sha256_clear(&sha); +} + +int secp256k1_iceberg_shares_gen(const secp256k1_context *ctx, secp256k1_iceberg_share * const *shares, unsigned int n, unsigned int t, const unsigned char *seed32) { + unsigned char seed[32]; + uint32_t total, rank; + unsigned int k; + + VERIFY_CHECK(ctx != NULL); + /* The out-params cannot be zeroed before n is validated, since n is what + * says how many of them there are. A caller that ignores the return value + * keeps whatever it passed in. */ + ARG_CHECK(shares != NULL); + ARG_CHECK(seed32 != NULL); + ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + /* The quorum needed to verify a sharing is 2t-1 and cannot exceed the + * group, which is what makes Iceberg a minority-threshold scheme. Rejecting + * here means callers meet the limitation at setup, not at signing. + * + * Written as t <= (n+1)/2 rather than 2t-1 <= n, which is the same + * predicate over the integers but not over unsigned int: t is the caller's + * to choose, and doubling it first lets a t near UINT_MAX wrap back inside + * the bound. The dealer would then find C(n, t-1) == 0, write no seeds at + * all, and return 1 on a group whose key is a public constant. */ + ARG_CHECK(t >= 1 && t <= (n + 1) / 2); + for (k = 1; k <= n; k++) { + ARG_CHECK(shares[k - 1] != NULL); + } + + /* One participant at a time. Its share is exactly the seeds whose subsets + * leave it out, appended in rank order. + * + * That ordering is the share layout, and it is not private to this function: + * secp256k1_rss_lagrange_weights_var walks the same subsets in the same order + * to produce one weight per seed, so seed i and weight i belong to each + * other. Dealing per participant costs a repeated derivation, since a + * subset's seed goes to every participant outside it, but the dealer runs + * once per group. */ + total = secp256k1_rss_binom(n, t - 1); + for (k = 1; k <= n; k++) { + size_t held = 0; + + secp256k1_iceberg_share_save(shares[k - 1], n, t, k); + for (rank = 0; rank < total; rank++) { + if (secp256k1_rss_subset_unrank(n, t - 1, rank) & (secp256k1_rss_subset)(1u << k)) { + continue; + } + secp256k1_iceberg_dealer_seed(ctx, seed, seed32, n, t, rank); + memcpy(&shares[k - 1]->data[8 + 32 * held], seed, 32); + held++; + } + } + + secp256k1_memclear_explicit(seed, sizeof(seed)); + return 1; +} + +int secp256k1_iceberg_share_serialize(const secp256k1_context *ctx, unsigned char *out, size_t *outlen, const secp256k1_iceberg_share *share) { + const unsigned char *seeds; + unsigned int n, t, k; + size_t count, needed; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(outlen != NULL); + ARG_CHECK(out != NULL); + ARG_CHECK(share != NULL); + + count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share); + if (count == 0) { + /* A malformed share has no length to report, and the caller is told to + * size a second call from *outlen. Leaving the old value there would + * send it round the same loop forever. */ + *outlen = 0; + return 0; + } + needed = 4 + 32 * count; + if (*outlen < needed) { + *outlen = needed; + return 0; + } + out[0] = 1; /* format version */ + out[1] = (unsigned char)n; + out[2] = (unsigned char)t; + out[3] = (unsigned char)k; + memcpy(&out[4], seeds, 32 * count); + *outlen = needed; + return 1; +} + +int secp256k1_iceberg_share_parse(const secp256k1_context *ctx, secp256k1_iceberg_share *share, const unsigned char *in, size_t inlen) { + unsigned int n, t, k; + size_t count; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(share != NULL); + memset(share, 0, sizeof(*share)); + ARG_CHECK(in != NULL); + + if (inlen < 4 || in[0] != 1) { + return 0; + } + n = in[1]; + t = in[2]; + k = in[3]; + if (n < 1 || n > SECP256K1_ICEBERG_MAX_PARTICIPANTS) { + return 0; + } + if (t < 1 || t > (n + 1) / 2 || k < 1 || k > n) { + return 0; + } + count = secp256k1_rss_binom(n - 1, t - 1); + if (inlen != 4 + 32 * count) { + return 0; + } + + secp256k1_iceberg_share_save(share, n, t, k); + memcpy(&share->data[8], &in[4], 32 * count); + return 1; +} + +static void secp256k1_iceberg_pubshare_save(secp256k1_iceberg_pubshare *pubshare, unsigned int k, const secp256k1_ge *ge) { + memcpy(pubshare->data, secp256k1_iceberg_pubshare_magic, 4); + pubshare->data[4] = (unsigned char)k; + secp256k1_ge_to_bytes_ext(&pubshare->data[5], ge); +} + +static int secp256k1_iceberg_pubshare_load(const secp256k1_context *ctx, unsigned int *k, secp256k1_ge *ge, const secp256k1_iceberg_pubshare *pubshare) { + ARG_CHECK(secp256k1_memcmp_var(pubshare->data, secp256k1_iceberg_pubshare_magic, 4) == 0); + *k = pubshare->data[4]; + ARG_CHECK(*k >= 1 && *k <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + secp256k1_ge_from_bytes_ext(ge, &pubshare->data[5]); + return 1; +} + +int secp256k1_iceberg_pubshare_gen(const secp256k1_context *ctx, secp256k1_iceberg_pubshare *pubshare, const secp256k1_iceberg_share *share, const secp256k1_iceberg_share_cache *cache) { + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS]; + secp256k1_scalar d; + secp256k1_gej dj; + secp256k1_ge point; + const unsigned char *seeds; + unsigned int n, t, k; + size_t count; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(pubshare != NULL); + memset(pubshare, 0, sizeof(*pubshare)); + ARG_CHECK(share != NULL); + ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx)); + + count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share); + if (count == 0 || !secp256k1_iceberg_weights_for(ctx, weights, count, n, t, k, cache)) { + return 0; + } + + secp256k1_rss_eval(secp256k1_get_hash_context(ctx), &d, seeds, weights, count, + secp256k1_iceberg_keygen_label, sizeof(secp256k1_iceberg_keygen_label)); + secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &dj, &d); + secp256k1_ge_set_gej(&point, &dj); + /* The commitment is about to be published, so it stops being secret here. */ + secp256k1_declassify(ctx, &point, sizeof(point)); + secp256k1_iceberg_pubshare_save(pubshare, k, &point); + + secp256k1_scalar_clear(&d); + return 1; +} + +int secp256k1_iceberg_pubshare_serialize(const secp256k1_context *ctx, unsigned char *out34, const secp256k1_iceberg_pubshare *pubshare) { + secp256k1_ge point; + unsigned int k; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out34 != NULL); + memset(out34, 0, 34); + ARG_CHECK(pubshare != NULL); + + if (!secp256k1_iceberg_pubshare_load(ctx, &k, &point, pubshare)) { + return 0; + } + out34[0] = (unsigned char)k; + secp256k1_musig_ge_serialize_ext(&out34[1], &point); + return 1; +} + +int secp256k1_iceberg_pubshare_parse(const secp256k1_context *ctx, secp256k1_iceberg_pubshare *pubshare, const unsigned char *in34) { + secp256k1_ge point; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(pubshare != NULL); + memset(pubshare, 0, sizeof(*pubshare)); + ARG_CHECK(in34 != NULL); + + if (in34[0] < 1 || in34[0] > SECP256K1_ICEBERG_MAX_PARTICIPANTS) { + return 0; + } + if (!secp256k1_musig_ge_parse_ext(&point, &in34[1])) { + return 0; + } + secp256k1_iceberg_pubshare_save(pubshare, in34[0], &point); + return 1; +} + +int secp256k1_iceberg_pubkey_agg(const secp256k1_context *ctx, secp256k1_pubkey *group_pk, const secp256k1_iceberg_pubshare * const *pubshares, size_t n_pubshares, unsigned int n, unsigned int t) { + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge points[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_gej combined; + secp256k1_ge result; + size_t i, j; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(group_pk != NULL); + memset(group_pk, 0, sizeof(*group_pk)); + ARG_CHECK(pubshares != NULL); + ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + ARG_CHECK(t >= 1 && t <= (n + 1) / 2); + /* Below 2t-1 the degree check proves nothing: too few honest points remain + * to pin the true polynomial, and a coalition could present a consistent + * sharing of a value it chose. Safe to double t here: the bound above has + * already put it under (n+1)/2. How many members published is a fact about + * the group rather than a caller bug, so it returns; a null entry in the + * sweep below is a caller bug and gets the illegal callback. + * + * More shares than the group has is a repeated index by the pigeonhole, so + * the loop below would refuse it anyway. The bound is here because idx and + * points are sized for the largest group. */ + if (n_pubshares < 2 * (size_t)t - 1 || n_pubshares > n) { + return 0; + } + for (i = 0; i < n_pubshares; i++) { + ARG_CHECK(pubshares[i] != NULL); + } + + for (i = 0; i < n_pubshares; i++) { + unsigned int k; + if (!secp256k1_iceberg_pubshare_load(ctx, &k, &points[i], pubshares[i])) { + return 0; + } + /* An index above n names no member of this group, so nothing could have + * authenticated what it carries. */ + if (k > n) { + return 0; + } + idx[i] = (unsigned char)k; + /* Duplicate indices would make the interpolation singular, and are how + * a caller most easily passes fewer distinct participants than it + * believes it has. */ + for (j = 0; j < i; j++) { + if (idx[j] == idx[i]) { + return 0; + } + } + } + + if (!secp256k1_vpss_verify_var(ctx, idx, points, n_pubshares, t)) { + return 0; + } + if (!secp256k1_vpss_combine_var(ctx, &combined, idx, points, n_pubshares)) { + return 0; + } + if (secp256k1_gej_is_infinity(&combined)) { + return 0; + } + secp256k1_ge_set_gej(&result, &combined); + secp256k1_pubkey_save(group_pk, &result); + return 1; +} + +#endif /* SECP256K1_MODULE_ICEBERG_KEYGEN_IMPL_H */ diff --git a/src/modules/iceberg/main_impl.h b/src/modules/iceberg/main_impl.h new file mode 100644 index 00000000..34518eb3 --- /dev/null +++ b/src/modules/iceberg/main_impl.h @@ -0,0 +1,22 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_MAIN_H +#define SECP256K1_MODULE_ICEBERG_MAIN_H + +/* Layers, bottom up. Each one may use those above it in this list and nothing + * below. That ordering is also the constant-time story: vpss sees only + * participant indices and published points and is variable time throughout, + * while scalar_poly has secret values passed through it and keeps them away + * from its inversions, as the note at the top of scalar_poly.h sets out. Seed + * material reaches rss_eval and the scalars keygen and session derive from it, + * which is where the clearing discipline lives. */ +#include "scalar_poly_impl.h" +#include "rss_impl.h" +#include "vpss_impl.h" +#include "keygen_impl.h" +#include "session_impl.h" + +#endif diff --git a/src/modules/iceberg/rss.h b/src/modules/iceberg/rss.h new file mode 100644 index 00000000..04466310 --- /dev/null +++ b/src/modules/iceberg/rss.h @@ -0,0 +1,141 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_RSS_H +#define SECP256K1_MODULE_ICEBERG_RSS_H + +#include + +#include "../../hash.h" +#include "../../scalar.h" +#include "../../../include/secp256k1_iceberg.h" +#include "scalar_poly.h" + +/* Replicated secret sharing, and the pseudorandom sharing built on top of it. + * + * The setup gives every (t-1)-subset of participants its own random seed, held + * by everyone outside that subset. A coalition of t-1 participants therefore + * holds every seed but one, the one named after the coalition itself, and + * that single unknown scalar is the scheme's entire security margin. + * + * From those seeds, any label w yields a fresh Shamir sharing with no + * communication at all: + * + * f_w(x) = sum over subsets a of H(phi_a, w) * L_a(x) + * + * L_a is one at zero and zero on every member of a, so f_w(0) is just the sum + * of the hashes, and participant k can evaluate f_w(k) alone: the terms it + * lacks the seeds for are exactly the terms L_a(k) sends to zero. That is what + * makes deterministic, stateless nonces possible. + * + * Of the three lower layers this is the only one that handles a secret: + * scalar_poly only has values passed through it and vpss sees none at all. + * Above it, keygen deals the seeds and session derives nonces from them, so the + * clearing discipline is theirs too. Seeds and the values derived from them are + * secret; participant indices, subset structure and every Lagrange weight are + * public, so work driven by those may be variable time. */ + +/* Derived bounds at ten participants. A quorum of 2t-1 cannot exceed the group, + * so t is at most (n+1)/2, which is 5; and the number of size-(t-1) subsets is + * largest at that maximum t, which is C(10, 4). C89 cannot evaluate a binomial + * at preprocessing time, so both are written out; the tests recompute them with + * secp256k1_rss_binom. */ +#define SECP256K1_ICEBERG_MAX_T 5 +#define SECP256K1_ICEBERG_MAX_SUBSETS 210 + +/* These three constants are the participant bound worked out by hand, so the + * compiler cannot see that they belong together. Raising the bound without + * recomputing them sizes the opaque types for one group and deals another, which + * overruns a share at run time instead of failing to build, so raising it is + * refused below. Lowering it is allowed: the suite checks all three against + * every configuration the new bound can express (tests_impl.h, + * run_iceberg_binom_test) and says which one is wrong. Drawing that distinction + * at preprocessing time is not possible, since C89 cannot evaluate a binomial. */ +#if SECP256K1_ICEBERG_MAX_PARTICIPANTS > 10 +#error "MAX_T and MAX_SUBSETS here, and MAX_SEEDS in secp256k1_iceberg.h, are written out for ten participants. Recompute all three before raising the bound." +#endif + +/* Lowering the bound is worth doing for a build that will only ever run small + * groups: at five participants a share is 200 bytes rather than 4040. Lower + * SECP256K1_ICEBERG_MAX_SEEDS to C(n-1, (n+1)/2 - 1) along with it, and MAX_T + * and MAX_SUBSETS above to match. + * + * Ten reaches every configuration the scheme can express up to 4-of-10, which is + * also the largest that clears the n >= 3t-2 bound a deployment needs. + * + * The same growth sizes the module's stack frames, which at ten participants + * come to nearly twice the largest frame anywhere in the musig module. The + * figures move with the compiler, so measure rather than assume: + * + * gcc -fstack-usage -O2 -c src/secp256k1.c -I. -Isrc -Iinclude $(module defines) + * sort -t$'\t' -k2 -rn secp256k1.su | head + * + * TODO: sizing every share for the compile-time maximum is what makes the bound + * an ABI decision. A secp256k1_iceberg_share_size(n, t) with caller-provided + * storage, the shape secp256k1_preallocated.h already uses, would let a group + * pay for the configuration it runs and would end that. */ + +/* The number of summands in a participant's evaluation is the number of seeds + * it holds. */ +#define SECP256K1_ICEBERG_MAX_SUMMANDS SECP256K1_ICEBERG_MAX_SEEDS + +/* The weights for a whole share are inverted in one batch, so the batch bound + * has to cover a participant's seed count. */ +#if SECP256K1_ICEBERG_MAX_SUMMANDS > SECP256K1_SCALARPOLY_MAX_BATCH +#error "SECP256K1_SCALARPOLY_MAX_BATCH is too small for the per-participant seed count" +#endif + +/* A quorum interpolates through one point per member, so the point bound has to + * cover the group. */ +#if SECP256K1_ICEBERG_MAX_PARTICIPANTS > SECP256K1_SCALARPOLY_MAX_POINTS +#error "SECP256K1_SCALARPOLY_MAX_POINTS is too small for the participant bound" +#endif + +/* A subset of participants as a bitmask, with bit j set when participant j + * belongs. Participants are numbered from one, so bit zero is always clear. */ +typedef uint16_t secp256k1_rss_subset; + +/* Binomial coefficient, for arguments within the compile-time bound. */ +static uint32_t secp256k1_rss_binom(unsigned int n, unsigned int size); + +/* Position of a subset in lexicographic order over all size-`size` subsets of + * {1..n}, and its inverse. At n = 5, size = 2 the order is + * + * {1,2} {1,3} {1,4} {1,5} {2,3} {2,4} {2,5} {3,4} {3,5} {4,5} + * 0 1 2 3 4 5 6 7 8 9 + * + * so {2,3} ranks 4. Neither function enumerates that list: ranking sums the + * binomials counting the subsets that come before, and unranking walks the same + * sum backwards. + * + * The ordering fixes the layout of a serialized share and which Lagrange weight + * pairs with which seed, so it is consensus-critical between implementations. + * The example above is written out because the reference's own docstring gives + * one that is off by one. `rank` here is a position in a list, unrelated to the + * rank of a matrix, which is the other thing the word means around this scheme. */ +static uint32_t secp256k1_rss_subset_rank(unsigned int n, unsigned int size, secp256k1_rss_subset subset); +static secp256k1_rss_subset secp256k1_rss_subset_unrank(unsigned int n, unsigned int size, uint32_t rank); + +/* r <- H(seed32 || w) as a scalar, using the "VPSS/prf" tag. Secret in, secret + * out. The reduction into the scalar field is unconditional, matching the + * reference implementation. */ +static void secp256k1_rss_prf(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seed32, const unsigned char *w, size_t wlen); + +/* weights[i] <- L_a(k) for the i'th subset a that participant k is outside of, + * in ascending rank order: the scalar its seed for a gets multiplied by, using + * the L_a defined at the top of this file. Public data throughout, so this is + * variable time and its result can be cached. Writes C(n-1, t-1) entries and + * returns that count. */ +static size_t secp256k1_rss_lagrange_weights_var(secp256k1_scalar *weights, unsigned int n, unsigned int t, unsigned int k); + +/* r <- f_w(k), the participant's share of the sharing labeled w. + * + * `seeds` is num 32-byte seeds and `weights` the matching output of + * secp256k1_rss_lagrange_weights_var, in the same order. Constant time in the + * seed values; the loop bound and access pattern depend only on public + * structure. */ +static void secp256k1_rss_eval(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seeds, const secp256k1_scalar *weights, size_t num, const unsigned char *w, size_t wlen); + +#endif /* SECP256K1_MODULE_ICEBERG_RSS_H */ diff --git a/src/modules/iceberg/rss_impl.h b/src/modules/iceberg/rss_impl.h new file mode 100644 index 00000000..9b1d0f5e --- /dev/null +++ b/src/modules/iceberg/rss_impl.h @@ -0,0 +1,181 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_RSS_IMPL_H +#define SECP256K1_MODULE_ICEBERG_RSS_IMPL_H + +#include "rss.h" +#include "scalar_poly_impl.h" + +#include "../../hash.h" +#include "../../scalar.h" +#include "../../util.h" + +/* C(n, k), zero outside the participant bound and above the diagonal. Computed + * rather than tabulated so it cannot disagree with MAX_PARTICIPANTS: a table + * sized for one bound and written out for another overruns below it and + * zero-fills above it, and a C(n, t-1) of zero makes the dealer write no seeds + * and report success. Both arguments count participants, never secret. */ +static uint32_t secp256k1_rss_binom(unsigned int n, unsigned int size) { + uint32_t result = 1; + unsigned int i; + + if (n > SECP256K1_ICEBERG_MAX_PARTICIPANTS || size > n) { + return 0; + } + if (size > n - size) { + size = n - size; + } + for (i = 0; i < size; i++) { + /* C(n, i+1) = C(n, i) * (n-i) / (i+1). The division is exact at every + * step because the running product is itself a binomial coefficient, and + * halving size above caps the intermediate at C(10, 4) * 6 = 1260 at + * the participant bound. */ + result = result * (n - i) / (i + 1); + } + return result; +} + +/* Lexicographic rank, walking the participants in order. Every participant we + * pass over without selecting skips the whole block of subsets that would have + * chosen it next. */ +static uint32_t secp256k1_rss_subset_rank(unsigned int n, unsigned int size, secp256k1_rss_subset subset) { + uint32_t rank = 0; + unsigned int remaining = size; + unsigned int j; + + for (j = 1; j <= n && remaining > 0; j++) { + if (subset & (secp256k1_rss_subset)(1u << j)) { + remaining--; + } else { + rank += secp256k1_rss_binom(n - j, remaining - 1); + } + } + return rank; +} + +static secp256k1_rss_subset secp256k1_rss_subset_unrank(unsigned int n, unsigned int size, uint32_t rank) { + secp256k1_rss_subset subset = 0; + unsigned int remaining = size; + unsigned int j = 1; + + while (remaining > 0) { + uint32_t block; + VERIFY_CHECK(j <= n); + block = secp256k1_rss_binom(n - j, remaining - 1); + if (rank < block) { + subset |= (secp256k1_rss_subset)(1u << j); + remaining--; + } else { + rank -= block; + } + j++; + } + return subset; +} + +/* The members of a subset, ascending, as the byte array the polynomial layer + * expects. Returns how many were written. */ +static size_t secp256k1_rss_subset_members(unsigned char *members, secp256k1_rss_subset subset, unsigned int n) { + size_t count = 0; + unsigned int j; + + for (j = 1; j <= n; j++) { + if (subset & (secp256k1_rss_subset)(1u << j)) { + members[count++] = (unsigned char)j; + } + } + return count; +} + +/* Initializes SHA256 with fixed midstate. This midstate was computed by applying + * SHA256 to SHA256("VPSS/prf")||SHA256("VPSS/prf"). */ +static void secp256k1_rss_prf_sha256_tagged(secp256k1_sha256 *sha) { + static const uint32_t midstate[8] = { + 0x2c0fc184ul, 0x5cc276f5ul, 0x96930a47ul, 0x1991257eul, + 0x5b0bb737ul, 0x8786890cul, 0x875ba8bbul, 0x6b6162bbul + }; + secp256k1_sha256_initialize_midstate(sha, 64, midstate); +} + +static void secp256k1_rss_prf(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seed32, const unsigned char *w, size_t wlen) { + secp256k1_sha256 sha; + unsigned char buf[32]; + + secp256k1_rss_prf_sha256_tagged(&sha); + secp256k1_sha256_write(hash_ctx, &sha, seed32, 32); + secp256k1_sha256_write(hash_ctx, &sha, w, wlen); + secp256k1_sha256_finalize(hash_ctx, &sha, buf); + + /* Reduce on overflow instead of rejecting. The bias is negligible and the + * reference implementation does the same, which matters because the test + * vectors have to agree byte for byte. */ + secp256k1_scalar_set_b32(r, buf, NULL); + + secp256k1_memclear_explicit(buf, sizeof(buf)); + secp256k1_sha256_clear(&sha); +} + +static size_t secp256k1_rss_lagrange_weights_var(secp256k1_scalar *weights, unsigned int n, unsigned int t, unsigned int k) { + secp256k1_scalar denominators[SECP256K1_ICEBERG_MAX_SUMMANDS]; + unsigned char members[SECP256K1_ICEBERG_MAX_T]; + uint32_t total = secp256k1_rss_binom(n, t - 1); + uint32_t rank; + size_t i, count = 0; + + VERIFY_CHECK(t >= 1 && k >= 1 && k <= n); + VERIFY_CHECK(n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + /* The scheme's own bound, and what keeps a subset inside members[]: a + * quorum of 2t-1 has to fit in the group, so t-1 < MAX_T. */ + VERIFY_CHECK(2 * t - 1 <= n); + + /* Walk every subset in rank order and keep the ones this participant holds + * a seed for, which is exactly those it does not belong to. Iterating the + * global order, instead of enumerating the complement directly, is what + * guarantees seeds and weights stay in step with the serialized share. + * + * The weights come out as fractions and are divided through at the end, one + * batch inversion for the whole share rather than one per weight. */ + for (rank = 0; rank < total; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(n, t - 1, rank); + size_t size; + if (subset & (secp256k1_rss_subset)(1u << k)) { + continue; + } + size = secp256k1_rss_subset_members(members, subset, n); + /* L_a(k): exclude nothing, evaluate at k. */ + secp256k1_scalarpoly_lagrange_parts_var(&weights[count], &denominators[count], members, size, 0, k); + count++; + } + + VERIFY_CHECK(count == secp256k1_rss_binom(n - 1, t - 1)); + + /* C(n-1, t-1) is never zero, but the batch inverter reads its first element + * unconditionally, so guard the empty run anyway. */ + if (count > 0) { + secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, count); + for (i = 0; i < count; i++) { + secp256k1_scalar_mul(&weights[i], &weights[i], &denominators[i]); + } + } + return count; +} + +static void secp256k1_rss_eval(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *r, const unsigned char *seeds, const secp256k1_scalar *weights, size_t num, const unsigned char *w, size_t wlen) { + secp256k1_scalar term; + size_t i; + + secp256k1_scalar_set_int(r, 0); + for (i = 0; i < num; i++) { + secp256k1_rss_prf(hash_ctx, &term, &seeds[32 * i], w, wlen); + /* Secret times public. The loop bound and the stride are both public, + * so nothing here branches or indexes on a seed value. */ + secp256k1_scalar_mul(&term, &term, &weights[i]); + secp256k1_scalar_add(r, r, &term); + } + secp256k1_scalar_clear(&term); +} + +#endif /* SECP256K1_MODULE_ICEBERG_RSS_IMPL_H */ diff --git a/src/modules/iceberg/scalar_poly.h b/src/modules/iceberg/scalar_poly.h new file mode 100644 index 00000000..f25d1268 --- /dev/null +++ b/src/modules/iceberg/scalar_poly.h @@ -0,0 +1,89 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_SCALAR_POLY_H +#define SECP256K1_MODULE_ICEBERG_SCALAR_POLY_H + +#include "../../scalar.h" + +/* Polynomials over the scalar field, represented as dense coefficient arrays + * with coeff[i] the coefficient of x^i. + * + * This layer knows nothing about elliptic curves, hashing, or threshold + * signing. It is a separate file so the Lagrange machinery can be audited + * against the paper on its own. + * + * Everything is variable time in the indices, which are participant numbers and + * therefore public. Coefficient values are touched with constant-time scalar + * operations, with one exception: every routine that inverts ends at + * secp256k1_scalar_inverse_var and is variable time in the value it inverts. + * That is lagrange_eval_var directly, and lagrange_basis_var and interpolate_at0 + * through inverse_batch_var. What they invert is always a Lagrange denominator, + * built from indices alone. A secret may pass through interpolate_at0's vals and + * through the multiply-and-add routines; a secret must never reach an + * inversion. */ + +/* An interpolation may involve at most this many points. Participant counts are + * bounded far below this by the C(n, t-1) share blowup; the slack costs only + * stack space in the basis computation. */ +#define SECP256K1_SCALARPOLY_MAX_POINTS 16 + +/* A batch inversion may involve at most this many scalars. It is not the point + * bound: the largest batch is one weight per seed a participant holds, which is + * C(n-1, t-1) and an order of magnitude larger. The consumer checks its own + * bound against this one at compile time. */ +#define SECP256K1_SCALARPOLY_MAX_BATCH 128 + +/* coeffs[0..m] <- prod_i (x - roots[i]), monic, so coeffs[m] is one. + * roots must be distinct and hold m entries; coeffs must hold m+1. */ +static void secp256k1_scalarpoly_from_roots_var(secp256k1_scalar *coeffs, const unsigned char *roots, size_t m); + +/* q[0..m-1] <- a(x) / (x - root), for monic a of degree m with a(root) == 0. + * The remainder is checked under VERIFY; a caller passing a root that is not a + * root has a bug, not a runtime error. */ +static void secp256k1_scalarpoly_div_root_var(secp256k1_scalar *q, const secp256k1_scalar *a, size_t m, unsigned char root); + +/* r <- prod_{i in idx, i != exclude} (i - at) / (i - exclude). + * + * One function covers both places Lagrange weights appear: + * + * exclude = 0, at = k gives L_a(k), the replicated-sharing weight that + * vanishes on every member of the set a + * exclude = j, at = 0 gives lambda_j, the weight that reconstructs f(0) + * + * They look unrelated in the papers and are the same product. + * + * The one-weight form. Anything computing many weights at once goes + * through lagrange_parts_var below instead and inverts them in a batch, so the + * one caller of this is on_degree_var, which needs a handful. */ +static void secp256k1_scalarpoly_lagrange_eval_var(secp256k1_scalar *r, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at); + +/* The same weight, left as a fraction: num/den, with no inversion performed. + * Inverting dominates the cost of a weight, so a caller computing many at once + * should collect the denominators and invert the batch. */ +static void secp256k1_scalarpoly_lagrange_parts_var(secp256k1_scalar *num, secp256k1_scalar *den, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at); + +/* basis[j*m + i] <- coefficient of x^i in the jth Lagrange basis polynomial for + * the node set idx. Needed only by the degree check, which inspects the high + * coefficients; the point evaluations above are cheaper for everything else. */ +static void secp256k1_scalarpoly_lagrange_basis_var(secp256k1_scalar *basis, const unsigned char *idx, size_t m); + +/* r <- sum_j lambda_j * vals[j], the interpolation of the given points at zero. + * idx is public and drives variable-time work; vals may be secret. */ +static void secp256k1_scalarpoly_interpolate_at0(secp256k1_scalar *r, const unsigned char *idx, const secp256k1_scalar *vals, size_t m); + +/* Do the m points (idx[i], vals[i]) lie on one polynomial of degree at most + * t-1? The first t of them fix that polynomial, so the question is only whether + * the other m-t agree with it, and at m == t there is nothing to ask. + * + * Both idx and vals must be public: this is variable time in the values, and it + * exists to check contributions that have already been published. */ +static int secp256k1_scalarpoly_on_degree_var(const unsigned char *idx, const secp256k1_scalar *vals, size_t m, size_t t); + +/* r[0..len-1] <- a[i]^-1, using one field inversion for the whole batch. + * All inputs must be non-zero. r and a may alias. */ +static void secp256k1_scalarpoly_inverse_batch_var(secp256k1_scalar *r, const secp256k1_scalar *a, size_t len); + +#endif /* SECP256K1_MODULE_ICEBERG_SCALAR_POLY_H */ diff --git a/src/modules/iceberg/scalar_poly_impl.h b/src/modules/iceberg/scalar_poly_impl.h new file mode 100644 index 00000000..4197dc48 --- /dev/null +++ b/src/modules/iceberg/scalar_poly_impl.h @@ -0,0 +1,223 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_SCALAR_POLY_IMPL_H +#define SECP256K1_MODULE_ICEBERG_SCALAR_POLY_IMPL_H + +#include "scalar_poly.h" + +#include "../../scalar.h" +#include "../../util.h" + +/* Participant indices are small positive integers, but differences between them + * are signed. Accumulating those differences in the scalar field instead of in + * a C integer keeps us out of overflow-analysis territory entirely: with indices + * up to 255 and sixteen points, an integer product would reach 2^120. */ +static void secp256k1_scalarpoly_small_diff(secp256k1_scalar *r, unsigned int a, unsigned int b) { + if (a >= b) { + secp256k1_scalar_set_int(r, a - b); + } else { + secp256k1_scalar_set_int(r, b - a); + secp256k1_scalar_negate(r, r); + } +} + +static void secp256k1_scalarpoly_from_roots_var(secp256k1_scalar *coeffs, const unsigned char *roots, size_t m) { + secp256k1_scalar neg_root, term; + size_t i, j; + + VERIFY_CHECK(m <= SECP256K1_SCALARPOLY_MAX_POINTS); + + /* Start from the constant polynomial 1 and multiply in one root at a time. + * Multiplying by (x - r) shifts every coefficient up and subtracts r times + * the original, so we walk downwards to avoid clobbering what we still need. */ + secp256k1_scalar_set_int(&coeffs[0], 1); + for (i = 0; i < m; i++) { + secp256k1_scalarpoly_small_diff(&neg_root, 0, roots[i]); + coeffs[i + 1] = coeffs[i]; + for (j = i; j > 0; j--) { + secp256k1_scalar_mul(&term, &coeffs[j], &neg_root); + secp256k1_scalar_add(&coeffs[j], &coeffs[j - 1], &term); + } + secp256k1_scalar_mul(&coeffs[0], &coeffs[0], &neg_root); + } +} + +static void secp256k1_scalarpoly_div_root_var(secp256k1_scalar *q, const secp256k1_scalar *a, size_t m, unsigned char root) { + secp256k1_scalar r, term; + size_t i; + + VERIFY_CHECK(m >= 1 && m <= SECP256K1_SCALARPOLY_MAX_POINTS); + secp256k1_scalar_set_int(&r, root); + + /* Synthetic division: q[i-1] = a[i] + root*q[i], seeded with the leading + * coefficient. What is left over is a(root), so the division is exact + * precisely when root is a root of a; whether a is monic plays no part. */ + q[m - 1] = a[m]; + for (i = m - 1; i > 0; i--) { + secp256k1_scalar_mul(&term, &q[i], &r); + secp256k1_scalar_add(&q[i - 1], &a[i], &term); + } + +#ifdef VERIFY + /* The remainder must vanish. If it does not, the caller passed a value that + * is not a root of a, which no correct call site can do. */ + secp256k1_scalar_mul(&term, &q[0], &r); + secp256k1_scalar_add(&term, &term, &a[0]); + VERIFY_CHECK(secp256k1_scalar_is_zero(&term)); +#endif +} + +static void secp256k1_scalarpoly_lagrange_parts_var(secp256k1_scalar *num, secp256k1_scalar *den, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at) { + secp256k1_scalar factor; + size_t i; + + secp256k1_scalar_set_int(num, 1); + secp256k1_scalar_set_int(den, 1); + for (i = 0; i < m; i++) { + if (idx[i] == exclude) { + continue; + } + secp256k1_scalarpoly_small_diff(&factor, idx[i], at); + secp256k1_scalar_mul(num, num, &factor); + secp256k1_scalarpoly_small_diff(&factor, idx[i], exclude); + secp256k1_scalar_mul(den, den, &factor); + } +} + +static void secp256k1_scalarpoly_lagrange_eval_var(secp256k1_scalar *r, const unsigned char *idx, size_t m, unsigned int exclude, unsigned int at) { + secp256k1_scalar numerator, denominator; + + secp256k1_scalarpoly_lagrange_parts_var(&numerator, &denominator, idx, m, exclude, at); + + /* Distinct indices make every factor non-zero. + * secp256k1_scalar_inverse_var maps zero to zero instead of failing, so a + * duplicate index would otherwise produce a silently wrong weight. */ + VERIFY_CHECK(!secp256k1_scalar_is_zero(&denominator)); + secp256k1_scalar_inverse_var(&denominator, &denominator); + secp256k1_scalar_mul(r, &numerator, &denominator); +} + +static int secp256k1_scalarpoly_on_degree_var(const unsigned char *idx, const secp256k1_scalar *vals, size_t m, size_t t) { + size_t j, k; + + VERIFY_CHECK(t >= 1 && m >= t); + + for (k = t; k < m; k++) { + secp256k1_scalar expected, weight, term; + + secp256k1_scalar_set_int(&expected, 0); + for (j = 0; j < t; j++) { + /* The weight of node j in the interpolant of the first t points, + * evaluated at the point being tested. idx[k] is not one of those + * nodes, since the caller has already refused a repeated index. */ + secp256k1_scalarpoly_lagrange_eval_var(&weight, idx, t, idx[j], idx[k]); + secp256k1_scalar_mul(&term, &weight, &vals[j]); + secp256k1_scalar_add(&expected, &expected, &term); + } + if (!secp256k1_scalar_eq(&expected, &vals[k])) { + return 0; + } + } + return 1; +} + +static void secp256k1_scalarpoly_lagrange_basis_var(secp256k1_scalar *basis, const unsigned char *idx, size_t m) { + secp256k1_scalar master[SECP256K1_SCALARPOLY_MAX_POINTS + 1]; + secp256k1_scalar denominators[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar factor; + size_t j, i; + + VERIFY_CHECK(m >= 1 && m <= SECP256K1_SCALARPOLY_MAX_POINTS); + + /* Fill the tail as well as the m entries set in the loop below. Only the + * first m are ever read, but inverse_batch_var accepts a len up to + * SECP256K1_SCALARPOLY_MAX_BATCH and GCC cannot see that m is bounded by + * the smaller SECP256K1_SCALARPOLY_MAX_POINTS, so it warns on the + * partially filled array. */ + for (j = 0; j < SECP256K1_SCALARPOLY_MAX_POINTS; j++) { + secp256k1_scalar_set_int(&denominators[j], 1); + } + + /* Every basis polynomial is the master polynomial with one root removed, so + * build the master once and divide it down m times, instead of forming m + * products from scratch. */ + secp256k1_scalarpoly_from_roots_var(master, idx, m); + for (j = 0; j < m; j++) { + secp256k1_scalarpoly_div_root_var(&basis[j * m], master, m, idx[j]); + secp256k1_scalar_set_int(&denominators[j], 1); + for (i = 0; i < m; i++) { + if (i == j) { + continue; + } + secp256k1_scalarpoly_small_diff(&factor, idx[j], idx[i]); + secp256k1_scalar_mul(&denominators[j], &denominators[j], &factor); + } + } + + secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, m); + for (j = 0; j < m; j++) { + for (i = 0; i < m; i++) { + secp256k1_scalar_mul(&basis[j * m + i], &basis[j * m + i], &denominators[j]); + } + } +} + +static void secp256k1_scalarpoly_interpolate_at0(secp256k1_scalar *r, const unsigned char *idx, const secp256k1_scalar *vals, size_t m) { + secp256k1_scalar numerators[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar denominators[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar term; + size_t j; + + VERIFY_CHECK(m >= 1 && m <= SECP256K1_SCALARPOLY_MAX_POINTS); + + /* Fill the tail as well: only the first m entries are read, but GCC cannot + * see that and warns on the partially filled array. */ + for (j = 0; j < SECP256K1_SCALARPOLY_MAX_POINTS; j++) { + secp256k1_scalar_set_int(&denominators[j], 1); + } + + /* One inversion for the whole quorum rather than one per weight. */ + for (j = 0; j < m; j++) { + secp256k1_scalarpoly_lagrange_parts_var(&numerators[j], &denominators[j], + idx, m, idx[j], 0); + } + secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, m); + + secp256k1_scalar_set_int(r, 0); + for (j = 0; j < m; j++) { + secp256k1_scalar_mul(&term, &numerators[j], &denominators[j]); + secp256k1_scalar_mul(&term, &term, &vals[j]); + secp256k1_scalar_add(r, r, &term); + } + secp256k1_scalar_clear(&term); +} + +static void secp256k1_scalarpoly_inverse_batch_var(secp256k1_scalar *r, const secp256k1_scalar *a, size_t len) { + secp256k1_scalar prefix[SECP256K1_SCALARPOLY_MAX_BATCH]; + secp256k1_scalar running; + size_t i; + + VERIFY_CHECK(len >= 1 && len <= SECP256K1_SCALARPOLY_MAX_BATCH); + + /* Montgomery's trick: invert the product of everything once, then peel the + * individual inverses off using the running prefix products. */ + prefix[0] = a[0]; + for (i = 1; i < len; i++) { + secp256k1_scalar_mul(&prefix[i], &prefix[i - 1], &a[i]); + } + VERIFY_CHECK(!secp256k1_scalar_is_zero(&prefix[len - 1])); + secp256k1_scalar_inverse_var(&running, &prefix[len - 1]); + + for (i = len - 1; i > 0; i--) { + secp256k1_scalar this_inverse; + secp256k1_scalar_mul(&this_inverse, &running, &prefix[i - 1]); + secp256k1_scalar_mul(&running, &running, &a[i]); + r[i] = this_inverse; + } + r[0] = running; +} + +#endif /* SECP256K1_MODULE_ICEBERG_SCALAR_POLY_IMPL_H */ diff --git a/src/modules/iceberg/session_impl.h b/src/modules/iceberg/session_impl.h new file mode 100644 index 00000000..781d1f73 --- /dev/null +++ b/src/modules/iceberg/session_impl.h @@ -0,0 +1,842 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_SESSION_IMPL_H +#define SECP256K1_MODULE_ICEBERG_SESSION_IMPL_H + +#include + +#include "../../../include/secp256k1_iceberg.h" + +#include "keygen_impl.h" +#include "rss_impl.h" +#include "scalar_poly_impl.h" +#include "vpss_impl.h" + +#include "../musig/keyagg.h" +#include "../musig/session.h" + +#include "../../group.h" +#include "../../hash.h" +#include "../../scalar.h" +#include "../../util.h" + +static const unsigned char secp256k1_iceberg_pubnonce_magic[4] = { 0x1c, 0xeb, 0x60, 0x2e }; +static const unsigned char secp256k1_iceberg_aggnonce_magic[4] = { 0x1c, 0xeb, 0xa9, 0x77 }; +static const unsigned char secp256k1_iceberg_psig_magic[4] = { 0x1c, 0xeb, 0x51, 0x8b }; +/* Initializes SHA256 with fixed midstate. This midstate was computed by applying + * SHA256 to SHA256("Iceberg/noncecoef")||SHA256("Iceberg/noncecoef"). */ +static void secp256k1_iceberg_noncecoef_sha256_tagged(secp256k1_sha256 *sha) { + static const uint32_t midstate[8] = { + 0x8848611aul, 0x6abc622ful, 0x7d0bbf3cul, 0x9d9c84faul, + 0x0188712dul, 0x04573e61ul, 0x23eb12e6ul, 0x88a27ee0ul + }; + secp256k1_sha256_initialize_midstate(sha, 64, midstate); +} + +/* The two VPSS labels a session uses, big-endian 1 and 2 followed by the + * session label. Sixty-four bytes each, matching the reference exactly. */ +static void secp256k1_iceberg_nonce_label(unsigned char *out64, unsigned int which, const unsigned char *sid32) { + memset(out64, 0, 32); + out64[31] = (unsigned char)which; + memcpy(&out64[32], sid32, 32); +} + +/* This participant's own two nonce points for a session: its share of each of + * the two nonce sharings, in the group. + * + * Round one publishes these; round two derives them again to check the set it + * was handed at its own index. That check means anything at all only if the two + * rounds agree on the derivation, so they use this rather than a copy each. The + * caller must have checked that the context carries an ecmult_gen table. + * + * k_out, if not NULL, receives the two scalars the points commit to. They are + * secret, and a caller that asks for them owns them and must clear them. Pass + * NULL to have them cleared here. + * + * The points are about to be published either way, so they are declassified. */ +static void secp256k1_iceberg_own_nonce_points(const secp256k1_context *ctx, secp256k1_ge *pts, secp256k1_scalar *k_out, const unsigned char *seeds, const secp256k1_scalar *weights, size_t count, const unsigned char *sid32) { + unsigned char label[64]; + int i; + + for (i = 0; i < 2; i++) { + secp256k1_scalar k_i; + secp256k1_gej pj; + secp256k1_iceberg_nonce_label(label, (unsigned int)(i + 1), sid32); + secp256k1_rss_eval(secp256k1_get_hash_context(ctx), &k_i, seeds, weights, count, label, sizeof(label)); + secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &pj, &k_i); + secp256k1_ge_set_gej(&pts[i], &pj); + secp256k1_declassify(ctx, &pts[i], sizeof(pts[i])); + if (k_out != NULL) { + k_out[i] = k_i; + } + secp256k1_scalar_clear(&k_i); + } +} + +/* b1 = H_Iceberg/noncecoef(R1 || R2' || P): the unscaled pre-nonces, then the + * group's key, each in musig's 33-byte extended encoding. The paper writes the + * arguments key first; the reference serializes them in this order and so do + * we, which the cross-implementation vectors pin. + * + * An inner aggregate has no binding of its own and the outer coefficient is + * computed too late to supply one, so every nesting level contributes a factor. + * Computed here and nowhere else, so the preimage moves in one place. */ +static void secp256k1_iceberg_noncecoef(const secp256k1_context *ctx, secp256k1_scalar *b1, secp256k1_ge *nonce_pts, const secp256k1_ge *group_pk) { + secp256k1_sha256 sha; + secp256k1_ge pk = *group_pk; + unsigned char buf[33]; + unsigned char out[32]; + + secp256k1_iceberg_noncecoef_sha256_tagged(&sha); + secp256k1_musig_ge_serialize_ext(buf, &nonce_pts[0]); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, buf, sizeof(buf)); + secp256k1_musig_ge_serialize_ext(buf, &nonce_pts[1]); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, buf, sizeof(buf)); + secp256k1_musig_ge_serialize_ext(buf, &pk); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &sha, buf, sizeof(buf)); + secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &sha, out); + secp256k1_scalar_set_b32(b1, out, NULL); +} + +/* The pair the group publishes, (R1, b1*R2'), from the internal one. Only the + * second point is scaled: at nesting depth one the coefficient enters as + * b1^(i-1), so the first is carried through untouched. + * + * Round one publishes this and round two rebuilds it from the aggregate it + * derives from the contributions. The two have to agree exactly or every + * signature the group produces is invalid, so it is written once rather than + * twice. + * + * 0 if either point comes out at infinity, which a MuSig2 public nonce has no + * encoding for. Both rounds refuse such a set because both reach it here: a + * contribution set can be consistent, agree with a signer's own contribution, + * and still interpolate to infinity at zero. */ +static int secp256k1_iceberg_publish_nonce(secp256k1_ge *out, const secp256k1_ge *pre, const secp256k1_scalar *b1) { + secp256k1_gej r2j, scaled; + + out[0] = pre[0]; + secp256k1_gej_set_ge(&r2j, &pre[1]); + secp256k1_ecmult(&scaled, &r2j, b1, NULL); + secp256k1_ge_set_gej(&out[1], &scaled); + return !secp256k1_ge_is_infinity(&out[0]) && !secp256k1_ge_is_infinity(&out[1]); +} + +static void secp256k1_iceberg_pubnonce_save(secp256k1_iceberg_pubnonce *nonce, unsigned int k, const secp256k1_ge *pts) { + memset(nonce->data, 0, sizeof(nonce->data)); + memcpy(nonce->data, secp256k1_iceberg_pubnonce_magic, 4); + nonce->data[4] = (unsigned char)k; + secp256k1_ge_to_bytes_ext(&nonce->data[5], &pts[0]); + secp256k1_ge_to_bytes_ext(&nonce->data[69], &pts[1]); +} + +static int secp256k1_iceberg_pubnonce_load(const secp256k1_context *ctx, unsigned int *k, secp256k1_ge *pts, const secp256k1_iceberg_pubnonce *nonce) { + ARG_CHECK(secp256k1_memcmp_var(nonce->data, secp256k1_iceberg_pubnonce_magic, 4) == 0); + *k = nonce->data[4]; + ARG_CHECK(*k >= 1 && *k <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + secp256k1_ge_from_bytes_ext(&pts[0], &nonce->data[5]); + secp256k1_ge_from_bytes_ext(&pts[1], &nonce->data[69]); + return 1; +} + +static void secp256k1_iceberg_aggnonce_save(secp256k1_iceberg_aggnonce *nonce, const secp256k1_ge *pts) { + memset(nonce->data, 0, sizeof(nonce->data)); + memcpy(nonce->data, secp256k1_iceberg_aggnonce_magic, 4); + secp256k1_ge_to_bytes_ext(&nonce->data[4], &pts[0]); + secp256k1_ge_to_bytes_ext(&nonce->data[68], &pts[1]); +} + +static int secp256k1_iceberg_aggnonce_load(const secp256k1_context *ctx, secp256k1_ge *pts, const secp256k1_iceberg_aggnonce *nonce) { + ARG_CHECK(secp256k1_memcmp_var(nonce->data, secp256k1_iceberg_aggnonce_magic, 4) == 0); + secp256k1_ge_from_bytes_ext(&pts[0], &nonce->data[4]); + secp256k1_ge_from_bytes_ext(&pts[1], &nonce->data[68]); + return 1; +} + +/* Read a set of nonce contributions: the index and both nonce points of each, + * refusing an index outside the group, a repeated index, or points that do not + * lie on a single sharing of degree t-1. */ +static int secp256k1_iceberg_contributions_load(const secp256k1_context *ctx, unsigned char *idx, secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS], const secp256k1_iceberg_pubnonce * const *pubnonces, size_t m, unsigned int n, unsigned int t) { + size_t i, j; + + if (n < 1 || n > SECP256K1_ICEBERG_MAX_PARTICIPANTS || t < 1 || t > (n + 1) / 2) { + return 0; + } + if (m < 2 * (size_t)t - 1 || m > n) { + return 0; + } + for (i = 0; i < m; i++) { + secp256k1_ge pts[2]; + unsigned int who; + + if (!secp256k1_iceberg_pubnonce_load(ctx, &who, pts, pubnonces[i])) { + return 0; + } + /* An index above n names no member of this group, so nothing could have + * authenticated what it carries. */ + if (who > n) { + return 0; + } + idx[i] = (unsigned char)who; + for (j = 0; j < i; j++) { + if (idx[j] == idx[i]) { + return 0; + } + } + points[0][i] = pts[0]; + points[1][i] = pts[1]; + } + for (i = 0; i < 2; i++) { + if (!secp256k1_vpss_verify_var(ctx, idx, points[i], m, t)) { + return 0; + } + } + return 1; +} + +/* Both sharings interpolated back to their constant term: the group's internal + * (R1, R2'). Only meaningful on a set contributions_load has accepted. */ +static int secp256k1_iceberg_contributions_combine(const secp256k1_context *ctx, secp256k1_ge *combined, const unsigned char *idx, secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS], size_t m) { + secp256k1_gej sum; + int i; + + for (i = 0; i < 2; i++) { + if (!secp256k1_vpss_combine_var(ctx, &sum, idx, points[i], m)) { + return 0; + } + secp256k1_ge_set_gej(&combined[i], &sum); + } + return 1; +} + +/* Wire formats. + * + * pubnonce 67 = index(1) | point(33) | point(33) + * aggnonce 66 = point(33) | point(33) + * partial_sig 33 = index(1) | scalar(32) + * + * Points use musig's extended serializer, which encodes infinity as 33 zero + * bytes. The aggregate can reach infinity, so the format has to express it, and + * a member's contribution uses the same encoding so the two do not differ + * without a reason. + * + * A member's objects carry its index inside the encoding, because everything + * downstream is indexed and pairing the two is then not something a caller can + * get wrong on the wire. The group's aggregate belongs to no member and carries + * none, which is the whole of the 67-versus-66 difference. */ +int secp256k1_iceberg_pubnonce_serialize(const secp256k1_context *ctx, unsigned char *out67, const secp256k1_iceberg_pubnonce *pubnonce) { + secp256k1_ge pts[2]; + unsigned int k; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out67 != NULL); + memset(out67, 0, 67); + ARG_CHECK(pubnonce != NULL); + + if (!secp256k1_iceberg_pubnonce_load(ctx, &k, pts, pubnonce)) { + return 0; + } + out67[0] = (unsigned char)k; + for (i = 0; i < 2; i++) { + secp256k1_musig_ge_serialize_ext(&out67[1 + 33 * i], &pts[i]); + } + return 1; +} + +int secp256k1_iceberg_pubnonce_parse(const secp256k1_context *ctx, secp256k1_iceberg_pubnonce *pubnonce, const unsigned char *in67) { + secp256k1_ge pts[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(pubnonce != NULL); + memset(pubnonce, 0, sizeof(*pubnonce)); + ARG_CHECK(in67 != NULL); + + if (in67[0] < 1 || in67[0] > SECP256K1_ICEBERG_MAX_PARTICIPANTS) { + return 0; + } + for (i = 0; i < 2; i++) { + if (!secp256k1_musig_ge_parse_ext(&pts[i], &in67[1 + 33 * i])) { + return 0; + } + } + secp256k1_iceberg_pubnonce_save(pubnonce, in67[0], pts); + return 1; +} + +int secp256k1_iceberg_aggnonce_serialize(const secp256k1_context *ctx, unsigned char *out66, const secp256k1_iceberg_aggnonce *aggnonce) { + secp256k1_ge pts[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out66 != NULL); + memset(out66, 0, 66); + ARG_CHECK(aggnonce != NULL); + + if (!secp256k1_iceberg_aggnonce_load(ctx, pts, aggnonce)) { + return 0; + } + for (i = 0; i < 2; i++) { + secp256k1_musig_ge_serialize_ext(&out66[33 * i], &pts[i]); + } + return 1; +} + +int secp256k1_iceberg_aggnonce_parse(const secp256k1_context *ctx, secp256k1_iceberg_aggnonce *aggnonce, const unsigned char *in66) { + secp256k1_ge pts[2]; + int i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(aggnonce != NULL); + memset(aggnonce, 0, sizeof(*aggnonce)); + ARG_CHECK(in66 != NULL); + + for (i = 0; i < 2; i++) { + if (!secp256k1_musig_ge_parse_ext(&pts[i], &in66[33 * i])) { + return 0; + } + } + secp256k1_iceberg_aggnonce_save(aggnonce, pts); + return 1; +} + +int secp256k1_iceberg_partial_sig_serialize(const secp256k1_context *ctx, unsigned char *out33, const secp256k1_iceberg_partial_sig *partial_sig) { + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(out33 != NULL); + memset(out33, 0, 33); + ARG_CHECK(partial_sig != NULL); + /* ARG_CHECK rather than a plain return, so that a malformed object reaches + * the caller the same way here as it does through the load helpers the four + * sibling serializers use. */ + ARG_CHECK(secp256k1_memcmp_var(partial_sig->data, secp256k1_iceberg_psig_magic, 4) == 0); + + out33[0] = partial_sig->data[4]; + memcpy(&out33[1], &partial_sig->data[5], 32); + return 1; +} + +int secp256k1_iceberg_partial_sig_parse(const secp256k1_context *ctx, secp256k1_iceberg_partial_sig *partial_sig, const unsigned char *in33) { + secp256k1_scalar s; + int overflow; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(partial_sig != NULL); + memset(partial_sig, 0, sizeof(*partial_sig)); + ARG_CHECK(in33 != NULL); + + if (in33[0] < 1 || in33[0] > SECP256K1_ICEBERG_MAX_PARTICIPANTS) { + return 0; + } + /* Reject an out-of-range scalar here instead of reducing it. Aggregation + * is linear, so a share that wrapped would combine into a signature that + * simply fails to verify. */ + secp256k1_scalar_set_b32(&s, &in33[1], &overflow); + if (overflow) { + return 0; + } + memcpy(partial_sig->data, secp256k1_iceberg_psig_magic, 4); + partial_sig->data[4] = in33[0]; + memcpy(&partial_sig->data[5], &in33[1], 32); + secp256k1_scalar_clear(&s); + return 1; +} + +int secp256k1_iceberg_nonce_gen(const secp256k1_context *ctx, secp256k1_iceberg_pubnonce *pubnonce, const secp256k1_iceberg_share *share, const secp256k1_iceberg_share_cache *cache, const unsigned char *sid32) { + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS]; + secp256k1_ge pts[2]; + const unsigned char *seeds; + unsigned int n, t, k; + size_t count; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(pubnonce != NULL); + memset(pubnonce, 0, sizeof(*pubnonce)); + ARG_CHECK(share != NULL); + ARG_CHECK(sid32 != NULL); + ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx)); + + count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share); + if (count == 0 || !secp256k1_iceberg_weights_for(ctx, weights, count, n, t, k, cache)) { + return 0; + } + + secp256k1_iceberg_own_nonce_points(ctx, pts, NULL, seeds, weights, count, sid32); + secp256k1_iceberg_pubnonce_save(pubnonce, k, pts); + return 1; +} + +int secp256k1_iceberg_nonce_agg(const secp256k1_context *ctx, secp256k1_musig_pubnonce *musig_pubnonce, secp256k1_iceberg_aggnonce *aggnonce, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, const secp256k1_pubkey *group_pk) { + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge combined[2], published[2], pk; + secp256k1_scalar b1; + size_t i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(musig_pubnonce != NULL); + memset(musig_pubnonce, 0, sizeof(*musig_pubnonce)); + /* Optional. Only the serializer reads an iceberg_aggnonce, so a caller with + * no use for one passes NULL instead of allocating 132 bytes to ignore. */ + if (aggnonce != NULL) { + memset(aggnonce, 0, sizeof(*aggnonce)); + } + ARG_CHECK(pubnonces != NULL); + ARG_CHECK(group_pk != NULL); + ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + ARG_CHECK(t >= 1 && t <= (n + 1) / 2); + /* Bounded here for the same reason as in partial_sign: the sweep has to stay + * inside the array, and contributions_load checks the quorum itself. */ + if (n_pubnonces > n) { + return 0; + } + for (i = 0; i < n_pubnonces; i++) { + ARG_CHECK(pubnonces[i] != NULL); + } + + if (!secp256k1_pubkey_load(ctx, &pk, group_pk)) { + return 0; + } + if (!secp256k1_iceberg_contributions_load(ctx, idx, points, pubnonces, n_pubnonces, n, t)) { + return 0; + } + if (!secp256k1_iceberg_contributions_combine(ctx, combined, idx, points, n_pubnonces)) { + return 0; + } + secp256k1_iceberg_noncecoef(ctx, &b1, combined, &pk); + if (!secp256k1_iceberg_publish_nonce(published, combined, &b1)) { + return 0; + } + /* Both out-parameters are written here rather than as each is ready, so that + * every path that returns 0 leaves them as the memset at the top left them. */ + if (aggnonce != NULL) { + secp256k1_iceberg_aggnonce_save(aggnonce, combined); + } + secp256k1_musig_pubnonce_save(musig_pubnonce, published); + return 1; +} + +/* Everything round two needs from the upper session, recomputed here rather + * than accepted from the caller. + * + * The aggregate nonce is assembled from the group's own exported contribution + * plus the cosigners', so "my nonce is in there" is true by construction. A + * signer that took a prepared session object instead would be trusting values + * an adversary can choose. + */ +static int secp256k1_iceberg_session_values(const secp256k1_context *ctx, secp256k1_scalar *b0b1, secp256k1_scalar *key_coef, int *fin_parity, const secp256k1_iceberg_aggnonce *aggnonce, const secp256k1_pubkey *group_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const unsigned char *msg32, const secp256k1_musig_aggnonce *cosigner_aggnonce) { + secp256k1_keyagg_cache_internal cache_i; + secp256k1_ge group_pts[2], cosigner_pts[2], total[2], pk; + secp256k1_scalar b1, b0, a, e; + secp256k1_gej acc; + unsigned char agg_pk32[32], fin_nonce[32]; + int i; + + if (!secp256k1_keyagg_cache_load(ctx, &cache_i, keyagg_cache)) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &pk, group_pk)) { + return 0; + } + if (!secp256k1_iceberg_aggnonce_load(ctx, group_pts, aggnonce)) { + return 0; + } + if (!secp256k1_musig_aggnonce_load(ctx, cosigner_pts, cosigner_aggnonce)) { + return 0; + } + + /* Re-derive the group's published nonce with the same function round one + * published it with, then add the cosigners' to it. */ + secp256k1_iceberg_noncecoef(ctx, &b1, group_pts, &pk); + if (!secp256k1_iceberg_publish_nonce(total, group_pts, &b1)) { + return 0; + } + for (i = 0; i < 2; i++) { + secp256k1_gej_set_ge(&acc, &total[i]); + secp256k1_gej_add_ge_var(&acc, &acc, &cosigner_pts[i], NULL); + secp256k1_ge_set_gej(&total[i], &acc); + } + + secp256k1_fe_get_b32(agg_pk32, &cache_i.pk.x); + secp256k1_musig_nonce_process_internal(ctx, fin_parity, fin_nonce, &b0, total, agg_pk32, msg32); + secp256k1_schnorrsig_challenge(secp256k1_get_hash_context(ctx), &e, fin_nonce, msg32, 32, agg_pk32); + + secp256k1_scalar_mul(b0b1, &b0, &b1); + + /* The key coefficient carries the aggregation weight and the parity + * bookkeeping from BIP-340: e * a * g * gacc, where the sign flips if the + * aggregate key is odd exactly once against the accumulated parity. */ + secp256k1_musig_keyaggcoef(secp256k1_get_hash_context(ctx), &a, &cache_i, &pk); + secp256k1_scalar_mul(key_coef, &e, &a); + if (secp256k1_fe_is_odd(&cache_i.pk.y) != cache_i.parity_acc) { + secp256k1_scalar_negate(key_coef, key_coef); + } + return 1; +} + +int secp256k1_iceberg_keyagg_check(const secp256k1_context *ctx, const secp256k1_musig_keyagg_cache *keyagg_cache, const secp256k1_pubkey * const *pubkeys, size_t n_pubkeys, const secp256k1_pubkey *group_pk) { + secp256k1_keyagg_cache_internal given, rebuilt; + secp256k1_musig_keyagg_cache scratch; + secp256k1_ge target; + int found = 0; + size_t i; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(keyagg_cache != NULL); + ARG_CHECK(pubkeys != NULL); + ARG_CHECK(group_pk != NULL); + ARG_CHECK(n_pubkeys >= 1); + for (i = 0; i < n_pubkeys; i++) { + ARG_CHECK(pubkeys[i] != NULL); + } + + if (!secp256k1_keyagg_cache_load(ctx, &given, keyagg_cache)) { + return 0; + } + if (!secp256k1_musig_pubkey_agg(ctx, NULL, &scratch, pubkeys, n_pubkeys)) { + return 0; + } + if (!secp256k1_keyagg_cache_load(ctx, &rebuilt, &scratch)) { + return 0; + } + /* pks_hash is the hash of the key list, fixed when the list is aggregated + * and untouched by any tweak applied afterwards, so this says the cache + * aggregates this list whatever has since been tweaked onto it. Rebuilding + * the cache from the list and using that instead would not do: the tweaks + * are not recoverable from the list, and it is the caller's cache that + * partial_sign will be working against. + * + * second_pk is also in the cache and is also fixed at aggregation, but it is + * derived from the same list, so comparing it as well could only fail on a + * hash collision. */ + if (secp256k1_memcmp_var(given.pks_hash, rebuilt.pks_hash, 32) != 0) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &target, group_pk)) { + return 0; + } + for (i = 0; i < n_pubkeys; i++) { + secp256k1_ge candidate; + if (!secp256k1_pubkey_load(ctx, &candidate, pubkeys[i])) { + return 0; + } + if (secp256k1_ge_eq_var(&candidate, &target)) { + found = 1; + } + } + return found; +} + +/* Rebuild the group's aggregate nonce from the contributions, and hand back the + * polynomial's value at `index`. Nothing is refused on that value here, and the + * two callers put it to opposite uses. Signing compares it against the + * contribution the signer derives for itself, in + * secp256k1_iceberg_verified_aggnonce below. Verification holds no share to + * derive one from and takes the value as the member's own nonce, which is what + * lets it check a member that published nothing in round one. + * + * A signer that accepts an aggregate from its coordinator is trusting a value it + * cannot check, and the nesting coefficient is a hash of exactly that value. + * Three fabricated aggregates under one label yield three equations in the same + * three unknowns; the third is the key. Deriving it here instead is what Arctic + * does, and the reason it does it. */ +static int secp256k1_iceberg_aggnonce_from(const secp256k1_context *ctx, secp256k1_iceberg_aggnonce *aggnonce, secp256k1_ge *at_index, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, unsigned int index) { + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge points[2][SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge combined[2]; + secp256k1_gej sum; + int i; + + if (!secp256k1_iceberg_contributions_load(ctx, idx, points, pubnonces, n_pubnonces, n, t)) { + return 0; + } + /* The degree check proves the set lies on one polynomial of the right + * degree, not which polynomial: a consistent set may be some other session's + * sharing. Evaluating at `index` gives the caller the one value that ties the + * set to a particular label. + * + * A caller that compares it deliberately does not have to have contributed. + * A member who was offline still holds the shares that fix what its + * contribution would have been. Requiring its presence would be stronger + * against an unauthenticated transport and would lock the offline member out; + * under the authenticated transport the scheme assumes it buys nothing, + * because among 2t-1 contributions that all name members, with at most t-1 + * corrupt, t are honest and t points already pin the polynomial. */ + for (i = 0; i < 2; i++) { + if (!secp256k1_vpss_eval_at_var(ctx, &sum, idx, points[i], n_pubnonces, index)) { + return 0; + } + secp256k1_ge_set_gej(&at_index[i], &sum); + } + if (!secp256k1_iceberg_contributions_combine(ctx, combined, idx, points, n_pubnonces)) { + return 0; + } + secp256k1_iceberg_aggnonce_save(aggnonce, combined); + return 1; +} + +/* The signer's use of the above, and the only place that value is compared: the + * set must agree, at this participant's index, with the contribution the + * participant derives for itself. */ +static int secp256k1_iceberg_verified_aggnonce(const secp256k1_context *ctx, secp256k1_iceberg_aggnonce *aggnonce, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, unsigned int own_index, const secp256k1_ge *own_pts) { + secp256k1_ge mine[2]; + int i; + + if (!secp256k1_iceberg_aggnonce_from(ctx, aggnonce, mine, pubnonces, n_pubnonces, n, t, own_index)) { + return 0; + } + for (i = 0; i < 2; i++) { + if (!secp256k1_ge_eq_var(&mine[i], &own_pts[i])) { + return 0; + } + } + return 1; +} + +/* The secrets secp256k1_iceberg_partial_sign holds. d and s are not yet + * meaningful on its early error paths; clearing them there writes zeros over + * whatever the stack held, which is what those paths want anyway. */ +static void secp256k1_iceberg_partial_sign_clear(secp256k1_scalar *nonce_k, secp256k1_scalar *d, secp256k1_scalar *s) { + secp256k1_scalar_clear(&nonce_k[0]); + secp256k1_scalar_clear(&nonce_k[1]); + secp256k1_scalar_clear(d); + secp256k1_scalar_clear(s); +} + +int secp256k1_iceberg_partial_sign(const secp256k1_context *ctx, secp256k1_iceberg_partial_sig *partial_sig, const secp256k1_iceberg_share *share, const secp256k1_iceberg_share_cache *cache, const unsigned char *sid32, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, const secp256k1_pubkey *group_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const unsigned char *msg32, const secp256k1_musig_aggnonce *cosigner_aggnonce) { + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS]; + secp256k1_scalar nonce_k[2], d, s, b0b1, key_coef; + secp256k1_iceberg_aggnonce aggnonce; + secp256k1_ge own_pts[2]; + const unsigned char *seeds; + unsigned int n, t, k; + size_t count, j; + int fin_parity; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(partial_sig != NULL); + memset(partial_sig, 0, sizeof(*partial_sig)); + ARG_CHECK(share != NULL); + ARG_CHECK(sid32 != NULL); + ARG_CHECK(pubnonces != NULL); + ARG_CHECK(group_pk != NULL); + ARG_CHECK(keyagg_cache != NULL); + ARG_CHECK(msg32 != NULL); + ARG_CHECK(cosigner_aggnonce != NULL); + ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx)); + /* The group size is read before the count is checked, because it is what the + * count has to be checked against. It comes off this participant's own + * share, so no caller and no peer gets to choose it. */ + count = secp256k1_iceberg_share_load(ctx, &n, &t, &k, &seeds, share); + if (count == 0) { + return 0; + } + /* The count is peer-influenced, so an impossible one returns 0 the way the + * rest of this call does, and the way the header says it will. It is bounded + * here, ahead of the quorum check in contributions_load, so that the sweep + * below stays inside an array a caller sized from n; a null entry is a caller + * bug and gets the illegal callback instead. partial_sig_verify orders these + * two the same way. */ + if (n_pubnonces > n) { + return 0; + } + for (j = 0; j < n_pubnonces; j++) { + ARG_CHECK(pubnonces[j] != NULL); + } + + /* sid32 is the caller's to choose and the caller's to keep unique. Three + * responses under one label are three equations in this participant's three + * secrets, and the third of them is its key share. Nothing here can detect + * that. doc/iceberg.md gives the rule in full. */ + + if (!secp256k1_iceberg_weights_for(ctx, weights, count, n, t, k, cache)) { + return 0; + } + + /* Recompute our own contribution so the set can be matched against it, and + * keep the scalars it commits to: they are what this participant signs + * with. */ + secp256k1_iceberg_own_nonce_points(ctx, own_pts, nonce_k, seeds, weights, count, sid32); + /* n comes off this participant's own share, so the signer bounds the + * contribution indices against the group it was actually dealt into rather + * than against whatever the coordinator would like the group to be. The + * nonce scalars are live from here on, so every return below clears them. */ + if (!secp256k1_iceberg_verified_aggnonce(ctx, &aggnonce, pubnonces, n_pubnonces, n, t, k, own_pts)) { + secp256k1_iceberg_partial_sign_clear(nonce_k, &d, &s); + return 0; + } + if (!secp256k1_iceberg_session_values(ctx, &b0b1, &key_coef, &fin_parity, + &aggnonce, group_pk, keyagg_cache, msg32, + cosigner_aggnonce)) { + secp256k1_iceberg_partial_sign_clear(nonce_k, &d, &s); + return 0; + } + + /* The third sharing this participant contributes to, keyed on the fixed + * label rather than the session's. Like the two nonce sharings above it, it + * is recomputed from the seeds rather than carried across from round one, so + * nothing secret had to survive the gap. */ + secp256k1_rss_eval(secp256k1_get_hash_context(ctx), &d, seeds, weights, count, + secp256k1_iceberg_keygen_label, sizeof(secp256k1_iceberg_keygen_label)); + + /* BIP-340 again: if the final nonce came out odd, both nonce terms flip. */ + if (fin_parity) { + secp256k1_scalar_negate(&nonce_k[0], &nonce_k[0]); + secp256k1_scalar_negate(&nonce_k[1], &nonce_k[1]); + } + + /* s_k = k1 + b0*b1*k2 + e*a*g*gacc*d_k */ + secp256k1_scalar_mul(&s, &key_coef, &d); + secp256k1_scalar_mul(&nonce_k[1], &b0b1, &nonce_k[1]); + secp256k1_scalar_add(&s, &s, &nonce_k[1]); + secp256k1_scalar_add(&s, &s, &nonce_k[0]); + + memcpy(partial_sig->data, secp256k1_iceberg_psig_magic, 4); + partial_sig->data[4] = (unsigned char)k; + secp256k1_scalar_get_b32(&partial_sig->data[5], &s); + + secp256k1_iceberg_partial_sign_clear(nonce_k, &d, &s); + return 1; +} + +int secp256k1_iceberg_partial_sig_verify(const secp256k1_context *ctx, const secp256k1_iceberg_partial_sig *partial_sig, const secp256k1_iceberg_pubshare *pubshare, const secp256k1_iceberg_pubnonce * const *pubnonces, size_t n_pubnonces, unsigned int n, unsigned int t, const secp256k1_pubkey *group_pk, const secp256k1_musig_keyagg_cache *keyagg_cache, const unsigned char *msg32, const secp256k1_musig_aggnonce *cosigner_aggnonce) { + secp256k1_iceberg_aggnonce aggnonce; + secp256k1_scalar b0b1, key_coef, s; + secp256k1_ge nonce_pts[2], d; + secp256k1_gej rj, dj, tmp; + unsigned int k; + size_t i; + int fin_parity; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(partial_sig != NULL); + ARG_CHECK(pubshare != NULL); + ARG_CHECK(pubnonces != NULL); + ARG_CHECK(group_pk != NULL); + ARG_CHECK(keyagg_cache != NULL); + ARG_CHECK(msg32 != NULL); + ARG_CHECK(cosigner_aggnonce != NULL); + ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + ARG_CHECK(t >= 1 && t <= (n + 1) / 2); + /* Bounded here for the same reason as in partial_sign: the sweep has to stay + * inside the array, and contributions_load checks the quorum itself. */ + if (n_pubnonces > n) { + return 0; + } + for (i = 0; i < n_pubnonces; i++) { + ARG_CHECK(pubnonces[i] != NULL); + } + + if (secp256k1_memcmp_var(partial_sig->data, secp256k1_iceberg_psig_magic, 4) != 0) { + return 0; + } + if (!secp256k1_iceberg_pubshare_load(ctx, &k, &d, pubshare)) { + return 0; + } + if (k > n) { + return 0; + } + /* The public share says which participant is being asked about. A signature + * share carrying some other index is refused rather than verified against + * its own, which catches the two arguments being drawn from different + * members. Not a privacy measure: parse accepts any index in range, so a + * share can be relabeled and tried against every public share until one + * verifies. Authorship was never hidden. */ + if (partial_sig->data[4] != k) { + return 0; + } + + /* Same aggregate the signer derived, and the participant's own nonce read + * off the same polynomial. Taking it from the set rather than as an argument + * is what lets this check a member who sat out round one: it published no + * contribution, and the set still fixes what its nonce had to be. */ + if (!secp256k1_iceberg_aggnonce_from(ctx, &aggnonce, nonce_pts, pubnonces, n_pubnonces, n, t, k)) { + return 0; + } + if (!secp256k1_iceberg_session_values(ctx, &b0b1, &key_coef, &fin_parity, + &aggnonce, group_pk, keyagg_cache, msg32, + cosigner_aggnonce)) { + return 0; + } + + /* s_k*G == +-(R1,k + b0*b1*R2,k) + e*a*g*gacc*D_k, rearranged so the whole + * check is one comparison against infinity. */ + secp256k1_gej_set_ge(&rj, &nonce_pts[1]); + secp256k1_ecmult(&rj, &rj, &b0b1, NULL); + secp256k1_gej_add_ge_var(&rj, &rj, &nonce_pts[0], NULL); + if (fin_parity) { + secp256k1_gej_neg(&rj, &rj); + } + + secp256k1_scalar_set_b32(&s, &partial_sig->data[5], NULL); + secp256k1_scalar_negate(&s, &s); + secp256k1_gej_set_ge(&dj, &d); + secp256k1_ecmult(&tmp, &dj, &key_coef, &s); + secp256k1_gej_add_var(&tmp, &tmp, &rj, NULL); + + return secp256k1_gej_is_infinity(&tmp); +} + + +int secp256k1_iceberg_partial_sig_agg(const secp256k1_context *ctx, secp256k1_musig_partial_sig *musig_partial_sig, const secp256k1_iceberg_partial_sig * const *partial_sigs, size_t n_partial_sigs, unsigned int n, unsigned int t) { + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar vals[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar s; + size_t i, j; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(musig_partial_sig != NULL); + memset(musig_partial_sig, 0, sizeof(*musig_partial_sig)); + ARG_CHECK(partial_sigs != NULL); + ARG_CHECK(n >= 1 && n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + ARG_CHECK(t >= 1 && t <= (n + 1) / 2); + /* Interpolation needs t points. The larger quorum belongs to the nonce + * round, where a degree check has to be sound; here it would only cost + * availability. How many members answered is a fact about the group rather + * than a caller bug, so it returns. The upper bound duplicates what the + * repeated-index check below would catch, and is here because idx and vals + * are sized for the largest group. */ + if (n_partial_sigs < t || n_partial_sigs > n) { + return 0; + } + for (i = 0; i < n_partial_sigs; i++) { + ARG_CHECK(partial_sigs[i] != NULL); + } + + for (i = 0; i < n_partial_sigs; i++) { + ARG_CHECK(secp256k1_memcmp_var(partial_sigs[i]->data, secp256k1_iceberg_psig_magic, 4) == 0); + idx[i] = partial_sigs[i]->data[4]; + if (idx[i] < 1 || idx[i] > n) { + return 0; + } + for (j = 0; j < i; j++) { + if (idx[j] == idx[i]) { + return 0; + } + } + secp256k1_scalar_set_b32(&vals[i], &partial_sigs[i]->data[5], NULL); + } + + /* The shares of one session lie on a degree t-1 polynomial, as the nonce round's contributions do, though the + * shares are a linear combination of three degree t-1 sharings with + * group-level coefficients and so lie on a degree t-1 polynomial too. It + * only bites above the threshold: at exactly t shares the interpolation is + * determined and there is nothing to disagree with, which is why passing one + * more share can turn a success into a refusal. A caller with a spare share + * learns that the set contradicts itself here, + * rather than from a signature that fails without naming a share. + * + * This says the set is self-consistent, not that the shares are the ones the + * members would have produced; secp256k1_iceberg_partial_sig_verify answers + * that, one share at a time, against the public share it names. */ + if (!secp256k1_scalarpoly_on_degree_var(idx, vals, n_partial_sigs, t)) { + return 0; + } + secp256k1_scalarpoly_interpolate_at0(&s, idx, vals, n_partial_sigs); + secp256k1_musig_partial_sig_save(musig_partial_sig, &s); + return 1; +} + +#endif /* SECP256K1_MODULE_ICEBERG_SESSION_IMPL_H */ diff --git a/src/modules/iceberg/tests_impl.h b/src/modules/iceberg/tests_impl.h new file mode 100644 index 00000000..30d3ad95 --- /dev/null +++ b/src/modules/iceberg/tests_impl.h @@ -0,0 +1,2432 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_TESTS_IMPL_H +#define SECP256K1_MODULE_ICEBERG_TESTS_IMPL_H + +#include + +#include "../../../include/secp256k1_iceberg.h" +#include "../../../include/secp256k1_iceberg_dealer.h" + +#include "../../scalar.h" +#include "../../testrand.h" +#include "../../testutil.h" +#include "../../unit_test.h" + +#include "vectors.h" + +/* The configuration tables below are written for a participant bound of ten. + * That bound is fixed in the public header, so every one of these is false + * today; they are here so that a build which edits the #define runs the subset + * of each table that still fits instead of tripping an ARG_CHECK. */ +static int iceberg_test_fits(unsigned int n) { + return n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; +} + +/* Horner evaluation, used only to check the library's own results against the + * defining property of whatever it just computed. */ +static void iceberg_test_poly_eval(secp256k1_scalar *r, const secp256k1_scalar *coeffs, size_t n_coeffs, unsigned int at) { + secp256k1_scalar x; + size_t i; + + secp256k1_scalar_set_int(&x, at); + secp256k1_scalar_set_int(r, 0); + for (i = n_coeffs; i > 0; i--) { + secp256k1_scalar_mul(r, r, &x); + secp256k1_scalar_add(r, r, &coeffs[i - 1]); + } +} + +static void iceberg_test_random_scalar(secp256k1_scalar *r) { + unsigned char buf[32]; + testrand256(buf); + secp256k1_scalar_set_b32(r, buf, NULL); +} + +/* The outer MuSig2 half of a session, which most of the tests below need before + * they can reach an iceberg call at all: a cosigner, and the aggregate of its + * key with the group's. The group's key enters as an ordinary public key. */ +static void iceberg_test_cosigner_key(secp256k1_keypair *keypair, secp256k1_pubkey *pk, + secp256k1_xonly_pubkey *agg_xonly, + secp256k1_musig_keyagg_cache *cache, + const secp256k1_pubkey *group_pk, + const unsigned char *seckey) { + const secp256k1_pubkey *pubkeys[2]; + + CHECK(secp256k1_keypair_create(CTX, keypair, seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, pk, keypair) == 1); + pubkeys[0] = group_pk; + pubkeys[1] = pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, agg_xonly, cache, pubkeys, 2) == 1); +} + +/* The cosigners' aggregate nonce, where the cosigners are the one made above. + * An aggregate of one is not a special case anywhere. */ +static void iceberg_test_cosigner_nonce(secp256k1_musig_secnonce *secnonce, + secp256k1_musig_pubnonce *pubnonce, + secp256k1_musig_aggnonce *aggnonce, + unsigned char *secrand, const unsigned char *seckey, + const secp256k1_pubkey *pk, const unsigned char *msg, + const secp256k1_musig_keyagg_cache *cache) { + const secp256k1_musig_pubnonce *just_cosigner[1]; + + CHECK(secp256k1_musig_nonce_gen(CTX, secnonce, pubnonce, secrand, seckey, pk, msg, cache, NULL) == 1); + just_cosigner[0] = pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, aggnonce, just_cosigner, 1) == 1); +} + +/* A polynomial built from roots must vanish at each of them, and not at a + * non-root: each round probes x = 11, which is never one. */ +static void run_iceberg_from_roots_test(void) { + unsigned char roots[5] = { 1, 3, 4, 7, 9 }; + secp256k1_scalar coeffs[6], value; + size_t i, j; + + for (i = 1; i <= 5; i++) { + secp256k1_scalarpoly_from_roots_var(coeffs, roots, i); + for (j = 0; j < i; j++) { + iceberg_test_poly_eval(&value, coeffs, i + 1, roots[j]); + CHECK(secp256k1_scalar_is_zero(&value)); + } + /* Monic, so the leading coefficient is one and the degree is exact. */ + CHECK(secp256k1_scalar_is_one(&coeffs[i])); + /* An index that is not a root must not vanish. */ + iceberg_test_poly_eval(&value, coeffs, i + 1, 11); + CHECK(!secp256k1_scalar_is_zero(&value)); + } +} + +/* Dividing out a root and multiplying it back must return the original. */ +static void run_iceberg_div_root_test(void) { + unsigned char roots[4] = { 2, 5, 6, 8 }; + secp256k1_scalar master[5], quotient[4], value, expected; + size_t j, at; + + secp256k1_scalarpoly_from_roots_var(master, roots, 4); + for (j = 0; j < 4; j++) { + secp256k1_scalarpoly_div_root_var(quotient, master, 4, roots[j]); + /* q(x)*(x - root) == master(x), checked at a handful of points. */ + for (at = 1; at <= 12; at++) { + secp256k1_scalar factor; + iceberg_test_poly_eval(&value, quotient, 4, (unsigned int)at); + secp256k1_scalarpoly_small_diff(&factor, (unsigned int)at, roots[j]); + secp256k1_scalar_mul(&value, &value, &factor); + iceberg_test_poly_eval(&expected, master, 5, (unsigned int)at); + CHECK(secp256k1_scalar_eq(&value, &expected)); + } + } +} + +/* The two uses of lagrange_eval_var, each against an independent computation of + * the same weight. These are the identities the whole scheme leans on, and both + * routes below share no code with lagrange_parts_var. + * + * L_a(a) == 1 is not tested here: at == exclude makes the numerator and + * denominator the same product, so it asserts x/x == 1 and any mutation that + * changes both loops together still satisfies it. */ +static void run_iceberg_lagrange_eval_test(void) { + unsigned char set[3] = { 2, 4, 5 }; + unsigned char quorum[4] = { 1, 3, 4, 6 }; + secp256k1_scalar poly[4], basis[4 * 4], weight, expect, scale, total; + unsigned int at; + size_t j; + + /* exclude = 0 is the replicated-sharing weight L_a, which rss_impl.h evaluates + * at a participant index outside a. Written as a polynomial the product over a + * is monic, so L_a(x) = P_a(x)/P_a(0) with P_a from from_roots_var, which + * run_iceberg_from_roots_test pins separately. L_a vanishing on every + * member of a (why a party can evaluate its own share without the seeds it + * lacks) is the special case P_a(i) == 0, so it is covered here too. */ + secp256k1_scalarpoly_from_roots_var(poly, set, 3); + iceberg_test_poly_eval(&scale, poly, 4, 0); + secp256k1_scalar_inverse_var(&scale, &scale); + for (at = 0; at <= 9; at++) { + secp256k1_scalarpoly_lagrange_eval_var(&weight, set, 3, 0, at); + iceberg_test_poly_eval(&expect, poly, 4, at); + secp256k1_scalar_mul(&expect, &expect, &scale); + CHECK(secp256k1_scalar_eq(&weight, &expect)); + } + + /* exclude = j is the reconstruction weight lambda_j. Nothing above reaches the + * skip in lagrange_parts_var, because 0 is never one of the indices; here it is + * taken on every iteration. lagrange_basis_var builds the same polynomials + * coefficient by coefficient and its own test pins it against poly_eval. */ + secp256k1_scalarpoly_lagrange_basis_var(basis, quorum, 4); + for (j = 0; j < 4; j++) { + for (at = 0; at <= 9; at++) { + secp256k1_scalarpoly_lagrange_eval_var(&weight, quorum, 4, quorum[j], at); + iceberg_test_poly_eval(&expect, &basis[j * 4], 4, at); + CHECK(secp256k1_scalar_eq(&weight, &expect)); + } + } + + /* And the reconstruction weights sum to one, for any quorum. */ + secp256k1_scalar_set_int(&total, 0); + for (j = 0; j < 4; j++) { + secp256k1_scalarpoly_lagrange_eval_var(&weight, quorum, 4, quorum[j], 0); + secp256k1_scalar_add(&total, &total, &weight); + } + CHECK(secp256k1_scalar_is_one(&total)); +} + +/* Each basis polynomial is one at its own node and zero at the others. */ +static void run_iceberg_lagrange_basis_test(void) { + unsigned char quorum[5] = { 1, 2, 4, 7, 9 }; + secp256k1_scalar basis[5 * 5], value; + size_t m, j, i; + + for (m = 1; m <= 5; m++) { + secp256k1_scalarpoly_lagrange_basis_var(basis, quorum, m); + for (j = 0; j < m; j++) { + for (i = 0; i < m; i++) { + iceberg_test_poly_eval(&value, &basis[j * m], m, quorum[i]); + if (i == j) { + CHECK(secp256k1_scalar_is_one(&value)); + } else { + CHECK(secp256k1_scalar_is_zero(&value)); + } + } + } + } +} + +/* Interpolating m evaluations of a degree m-1 polynomial must return its + * constant term. + * + * This runs all the way to SECP256K1_SCALARPOLY_MAX_POINTS deliberately. The + * Python reference computes the same weights through numpy float64 and starts + * returning wrong answers from a quorum of 19, with no indication that anything + * went wrong. Working in the scalar field makes the size irrelevant, and testing + * at the maximum is how we keep it that way. */ +static void run_iceberg_interpolate_test_internal(void) { + unsigned char quorum[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar coeffs[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar vals[SECP256K1_SCALARPOLY_MAX_POINTS]; + secp256k1_scalar recovered; + size_t m, i, trial; + + /* Spread the indices rather than using 1..m, so a bug that only shows up + * with consecutive nodes has somewhere to surface. */ + for (i = 0; i < SECP256K1_SCALARPOLY_MAX_POINTS; i++) { + quorum[i] = (unsigned char)(2 * i + 1); + } + + for (m = 1; m <= SECP256K1_SCALARPOLY_MAX_POINTS; m++) { + for (trial = 0; trial < 2; trial++) { + for (i = 0; i < m; i++) { + iceberg_test_random_scalar(&coeffs[i]); + } + for (i = 0; i < m; i++) { + iceberg_test_poly_eval(&vals[i], coeffs, m, quorum[i]); + } + secp256k1_scalarpoly_interpolate_at0(&recovered, quorum, vals, m); + CHECK(secp256k1_scalar_eq(&recovered, &coeffs[0])); + } + } +} + +static void run_iceberg_batch_inverse_test_internal(void) { + secp256k1_scalar values[6], batched[6], single; + size_t len, i; + + for (len = 1; len <= 6; len++) { + for (i = 0; i < len; i++) { + do { + iceberg_test_random_scalar(&values[i]); + } while (secp256k1_scalar_is_zero(&values[i])); + } + secp256k1_scalarpoly_inverse_batch_var(batched, values, len); + for (i = 0; i < len; i++) { + secp256k1_scalar_inverse_var(&single, &values[i]); + CHECK(secp256k1_scalar_eq(&batched[i], &single)); + } + /* In-place must give the same answer, since callers will want it. */ + secp256k1_scalarpoly_inverse_batch_var(values, values, len); + for (i = 0; i < len; i++) { + CHECK(secp256k1_scalar_eq(&batched[i], &values[i])); + } + } +} + +/* The binomial is computed rather than tabulated, so check it against the + * recurrence that defines it, and confirm the derived bounds in rss.h really + * are the worst case they claim to be. */ +static void run_iceberg_binom_test(void) { + unsigned int n, k, max_t = 0; + uint32_t max_summands = 0, max_subsets = 0; + + for (n = 0; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + CHECK(secp256k1_rss_binom(n, 0) == 1); + CHECK(secp256k1_rss_binom(n, n) == 1); + for (k = 1; k < n; k++) { + CHECK(secp256k1_rss_binom(n, k) + == secp256k1_rss_binom(n - 1, k - 1) + secp256k1_rss_binom(n - 1, k)); + } + } + + /* A quorum of 2t-1 must fit in the group, so t <= (n+1)/2. Over every + * expressible configuration, no threshold exceeds MAX_T, no participant + * holds more seeds than MAX_SUMMANDS and no group has more subsets than + * MAX_SUBSETS. All three are written out by hand, since C89 cannot evaluate + * a binomial at preprocessing time, so all three are recomputed here. + * + * Each is pinned by equality rather than bounded. Too small overruns an + * array and the bound alone would catch that; too large only wastes space, + * which is a real thing to get told about when the participant bound has + * been lowered and one of these was left behind. */ + for (n = 1; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + unsigned int t; + for (t = 1; 2 * t - 1 <= n; t++) { + CHECK(t <= SECP256K1_ICEBERG_MAX_T); + if (t > max_t) { + max_t = t; + } + if (secp256k1_rss_binom(n - 1, t - 1) > max_summands) { + max_summands = secp256k1_rss_binom(n - 1, t - 1); + } + if (secp256k1_rss_binom(n, t - 1) > max_subsets) { + max_subsets = secp256k1_rss_binom(n, t - 1); + } + } + } + CHECK(max_t == SECP256K1_ICEBERG_MAX_T); + CHECK(max_summands == SECP256K1_ICEBERG_MAX_SUMMANDS); + CHECK(max_subsets == SECP256K1_ICEBERG_MAX_SUBSETS); +} + +/* Ranking fixes the wire layout of a share, so it has to be a bijection and it + * has to agree with lexicographic order. Both checked exhaustively. */ +static void run_iceberg_subset_rank_test(void) { + unsigned int n, size; + + for (n = 1; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + for (size = 1; size <= n && size <= SECP256K1_ICEBERG_MAX_T; size++) { + uint32_t total = secp256k1_rss_binom(n, size); + unsigned char previous[SECP256K1_ICEBERG_MAX_T]; + unsigned char current[SECP256K1_ICEBERG_MAX_T]; + uint32_t rank; + + for (rank = 0; rank < total; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(n, size, rank); + size_t members = secp256k1_rss_subset_members(current, subset, n); + CHECK(members == size); + CHECK(secp256k1_rss_subset_rank(n, size, subset) == rank); + if (rank > 0) { + /* Strictly increasing in lexicographic order. */ + size_t i = 0; + while (i < size && previous[i] == current[i]) { + i++; + } + CHECK(i < size && previous[i] < current[i]); + } + memcpy(previous, current, size); + } + } + } +} + +/* Known answers taken from the reference implementation's own prf(). If these + * drift, shares stop being portable between implementations. */ +static void run_iceberg_prf_test(void) { + static const unsigned char expected_hello[32] = { + 0xdf,0x15,0xf1,0x11,0xc7,0xd3,0xe8,0x7e,0x32,0x58,0x82,0x72,0xa0,0x9d,0x4f,0x25, + 0xaa,0x27,0x35,0xb5,0x4d,0x56,0x7e,0xbc,0xcd,0x10,0x29,0x79,0x01,0x74,0x71,0xbe + }; + static const unsigned char expected_empty[32] = { + 0x20,0x15,0x27,0x95,0xd7,0x00,0x9c,0x6f,0x3c,0x7c,0x20,0x69,0x3e,0xf9,0xfc,0x51, + 0x63,0x8c,0x64,0x31,0x8e,0xb1,0xc2,0x65,0x1f,0x5f,0xd2,0x48,0xaf,0x05,0x9f,0x14 + }; + unsigned char seed[32], out[32]; + secp256k1_scalar r; + + memset(seed, 0x01, sizeof(seed)); + secp256k1_rss_prf(secp256k1_get_hash_context(CTX), &r, seed, (const unsigned char *)"hello", 5); + secp256k1_scalar_get_b32(out, &r); + CHECK(secp256k1_memcmp_var(out, expected_hello, 32) == 0); + + memset(seed, 0xab, sizeof(seed)); + secp256k1_rss_prf(secp256k1_get_hash_context(CTX), &r, seed, NULL, 0); + secp256k1_scalar_get_b32(out, &r); + CHECK(secp256k1_memcmp_var(out, expected_empty, 32) == 0); + +} + +/* Advance `chosen` to the next size-`size` subset of {1..n}, ascending. + * Returns 0 once the last one has been passed. */ +static int iceberg_test_next_combination(unsigned char *chosen, size_t size, unsigned int n) { + size_t i = size; + + while (i > 0) { + i--; + if (chosen[i] < n - (size - 1 - i)) { + size_t j; + chosen[i]++; + for (j = i + 1; j < size; j++) { + chosen[j] = chosen[j - 1] + 1; + } + return 1; + } + } + return 0; +} + +/* Every precomputed midstate equals its tag applied at runtime. + * + * Each of the four is eight magic constants a person pasted in, and three of + * them are only pinned by accident: dealer by the known-answer test, noncecoef + * by the vectors, prf by the two digests above. Iceberg/batchcoef was pinned by + * nothing at all, because the batching coefficient only has to be unpredictable: + * any midstate produces a sound check, so a wrong one would have been invisible + * here and two implementations would have disagreed in silence. */ +static void run_iceberg_midstate_test(void) { + static void (* const tagged[])(secp256k1_sha256 *) = { + secp256k1_rss_prf_sha256_tagged, + secp256k1_vpss_batchcoef_sha256_tagged, + secp256k1_iceberg_dealer_sha256_tagged, + secp256k1_iceberg_noncecoef_sha256_tagged + }; + static const char * const tags[] = { + "VPSS/prf", "Iceberg/batchcoef", "Iceberg/dealer", "Iceberg/noncecoef" + }; + unsigned char probe[32]; + size_t i; + + memset(probe, 0x5e, sizeof(probe)); + for (i = 0; i < sizeof(tags) / sizeof(tags[0]); i++) { + secp256k1_sha256 from_midstate, from_tag; + unsigned char a[32], b[32]; + + tagged[i](&from_midstate); + secp256k1_sha256_initialize_tagged(secp256k1_get_hash_context(CTX), &from_tag, + (const unsigned char *)tags[i], strlen(tags[i])); + secp256k1_sha256_write(secp256k1_get_hash_context(CTX), &from_midstate, probe, sizeof(probe)); + secp256k1_sha256_write(secp256k1_get_hash_context(CTX), &from_tag, probe, sizeof(probe)); + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &from_midstate, a); + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &from_tag, b); + CHECK(secp256k1_memcmp_var(a, b, 32) == 0); + } +} + +/* The outer key aggregation, which partial_sign cannot check for itself. + * + * It takes the group's public key and the MuSig2 cache as two separate arguments + * and nothing ties them together, because nothing can: a cache records the hash + * of the key list rather than the list. keyagg_check answers the question + * directly, and belongs where the cache is built rather than once per signature, + * since the key set belongs to the channel. */ +static void run_iceberg_keyagg_check_test(void) { + enum { N = 7, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[MU]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_keypair cosigner_keypair; + secp256k1_pubkey group_pk, cosigner_pk, impostor; + const secp256k1_pubkey *pubkeys[2], *wrong[2]; + unsigned char seed[32], seckey[32], tweak[32]; + unsigned int k; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x11, sizeof(seed)); + memset(seckey, 0x22, sizeof(seckey)); + + for (k = 1; k <= N; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + } + for (i = 0; i < MU; i++) { + pubshare_ptrs[i] = &pubshares[i]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + CHECK(secp256k1_keypair_create(CTX, &cosigner_keypair, seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, &cosigner_pk, &cosigner_keypair) == 1); + /* A key the signer has no way to check against anything it holds. */ + seckey[0] ^= 1; + CHECK(secp256k1_keypair_create(CTX, &cosigner_keypair, seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, &impostor, &cosigner_keypair) == 1); + + pubkeys[0] = &group_pk; + pubkeys[1] = &cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, NULL, &keyagg_cache, pubkeys, 2) == 1); + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk) == 1); + + /* The same two keys in the other order hash to something else. */ + pubkeys[0] = &cosigner_pk; + pubkeys[1] = &group_pk; + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk) == 0); + + /* The right list, but the group is not in it. */ + pubkeys[0] = &group_pk; + pubkeys[1] = &cosigner_pk; + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &impostor) == 0); + + /* A cosigner the cache was not built over. */ + wrong[0] = &group_pk; + wrong[1] = &impostor; + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, wrong, 2, &group_pk) == 0); + + /* Tweaks do not disturb it: the list hash is fixed at aggregation. */ + memset(tweak, 0x6e, sizeof(tweak)); + CHECK(secp256k1_musig_pubkey_xonly_tweak_add(CTX, NULL, &keyagg_cache, tweak) == 1); + CHECK(secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk) == 1); +} + + +/* The identity the whole scheme rests on: participants who never communicate + * produce evaluations of one degree t-1 polynomial whose constant term is the + * sum of the hashes of every seed. Checked for every quorum, so "any t of them + * agree" is exhaustive rather than sampled. */ +static void run_iceberg_pss_test_internal(void) { + static const unsigned char configs[][2] = { {3,2}, {4,2}, {5,3}, {6,3}, {7,4} }; + unsigned char seeds[SECP256K1_ICEBERG_MAX_SUBSETS][32]; + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SUMMANDS]; + unsigned char held[SECP256K1_ICEBERG_MAX_SUMMANDS * 32]; + secp256k1_scalar shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1]; + unsigned char label[8] = "label"; + size_t config; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1]; + uint32_t total = secp256k1_rss_binom(n, t - 1); + secp256k1_scalar expected, term, recovered; + unsigned char quorum[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar quorum_vals[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + uint32_t rank; + unsigned int k; + size_t i; + + if (!iceberg_test_fits(n)) { + continue; + } + + for (rank = 0; rank < total; rank++) { + testrand256(seeds[rank]); + } + + /* f_w(0) is just the sum over every seed. No participant computes this, + * and no participant could. */ + secp256k1_scalar_set_int(&expected, 0); + for (rank = 0; rank < total; rank++) { + secp256k1_rss_prf(secp256k1_get_hash_context(CTX), &term, seeds[rank], label, 5); + secp256k1_scalar_add(&expected, &expected, &term); + } + + for (k = 1; k <= n; k++) { + size_t count = secp256k1_rss_lagrange_weights_var(weights, n, t, k); + size_t got = 0; + CHECK(count == secp256k1_rss_binom(n - 1, t - 1)); + /* Gather this participant's seeds in the same rank order the weights + * were produced in. */ + for (rank = 0; rank < total; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(n, t - 1, rank); + if (subset & (secp256k1_rss_subset)(1u << k)) { + continue; + } + memcpy(&held[32 * got], seeds[rank], 32); + got++; + } + CHECK(got == count); + secp256k1_rss_eval(secp256k1_get_hash_context(CTX), &shares[k], held, weights, count, label, 5); + } + + /* Every t-subset of participants must reconstruct the same value. */ + for (i = 0; i < t; i++) { + quorum[i] = (unsigned char)(i + 1); + } + do { + for (i = 0; i < t; i++) { + quorum_vals[i] = shares[quorum[i]]; + } + secp256k1_scalarpoly_interpolate_at0(&recovered, quorum, quorum_vals, t); + CHECK(secp256k1_scalar_eq(&recovered, &expected)); + } while (iceberg_test_next_combination(quorum, t, n)); + } +} + +/* The degree check, written out the way the definition reads: one + * multiexponentiation per high coefficient, no batching, no transcript. + * + * This exists only to disagree with the batched implementation if the batching + * is ever wrong, so it is deliberately a transcription of the defining property + * rather than a refactor of the real code. It does share lagrange_basis_var with + * the real one, which run_iceberg_lagrange_basis_test pins separately against + * its defining property; an error there would pass both of these. + */ +static int iceberg_test_verify_naive(const unsigned char *idx, const secp256k1_ge *points, size_t m, unsigned int t) { + secp256k1_scalar basis[SECP256K1_ICEBERG_MAX_PARTICIPANTS * SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + size_t i, j; + + secp256k1_scalarpoly_lagrange_basis_var(basis, idx, m); + for (i = t; i < m; i++) { + secp256k1_gej acc; + secp256k1_gej_set_infinity(&acc); + for (j = 0; j < m; j++) { + secp256k1_gej term, pointj; + secp256k1_gej_set_ge(&pointj, &points[j]); + secp256k1_ecmult(&term, &pointj, &basis[j * m + i], NULL); + secp256k1_gej_add_var(&acc, &acc, &term, NULL); + } + if (!secp256k1_gej_is_infinity(&acc)) { + return 0; + } + } + return 1; +} + +/* Commit to a polynomial: points[i] <- f(idx[i]) * G. */ +static void iceberg_test_commitments(secp256k1_ge *points, const unsigned char *idx, size_t m, const secp256k1_scalar *coeffs, size_t degree_plus_one) { + size_t i; + for (i = 0; i < m; i++) { + secp256k1_scalar value; + secp256k1_gej pointj; + iceberg_test_poly_eval(&value, coeffs, degree_plus_one, idx[i]); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &pointj, &value); + secp256k1_ge_set_gej(&points[i], &pointj); + } +} + +static void run_iceberg_vpss_test_internal(void) { + unsigned char idx[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_ge points[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar coeffs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned int t; + size_t m, i; + + for (i = 0; i < SECP256K1_ICEBERG_MAX_PARTICIPANTS; i++) { + idx[i] = (unsigned char)(i + 1); + } + + for (t = 2; t <= 4; t++) { + for (m = 2 * t - 1; m <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; m++) { + secp256k1_ge tampered[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_gej combined, expected_gej; + secp256k1_ge expected; + secp256k1_scalar high[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + + for (i = 0; i < t; i++) { + iceberg_test_random_scalar(&coeffs[i]); + } + iceberg_test_commitments(points, idx, m, coeffs, t); + + /* Honest commitments verify, and the batched check agrees with the + * term-by-term one. */ + CHECK(secp256k1_vpss_verify_var(CTX, idx, points, m, t) == 1); + CHECK(iceberg_test_verify_naive(idx, points, m, t) == 1); + + /* Combining gives the commitment to the secret, from any quorum. */ + CHECK(secp256k1_vpss_combine_var(CTX, &combined, idx, points, m) == 1); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &expected_gej, &coeffs[0]); + secp256k1_ge_set_gej(&expected, &expected_gej); + CHECK(secp256k1_gej_eq_ge_var(&combined, &expected)); + + /* One participant lying pushes the degree up and is caught. Both + * implementations must agree that it was. */ + memcpy(tampered, points, sizeof(secp256k1_ge) * m); + { + secp256k1_gej bumped; + secp256k1_scalar delta; + iceberg_test_random_scalar(&delta); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &bumped, &delta); + secp256k1_gej_add_ge_var(&bumped, &bumped, &tampered[m / 2], NULL); + secp256k1_ge_set_gej(&tampered[m / 2], &bumped); + } + CHECK(secp256k1_vpss_verify_var(CTX, idx, tampered, m, t) == 0); + CHECK(iceberg_test_verify_naive(idx, tampered, m, t) == 0); + + /* A genuine degree-t sharing must also fail. The tamper case + * above only shows that inconsistent points are caught; this shows + * the check bounds the degree rather than merely testing that the + * points lie on some polynomial. */ + if (m > t) { + for (i = 0; i <= t; i++) { + iceberg_test_random_scalar(&high[i]); + } + do { + iceberg_test_random_scalar(&high[t]); + } while (secp256k1_scalar_is_zero(&high[t])); + iceberg_test_commitments(points, idx, m, high, t + 1); + CHECK(secp256k1_vpss_verify_var(CTX, idx, points, m, t) == 0); + CHECK(iceberg_test_verify_naive(idx, points, m, t) == 0); + } + } + } +} + +/* Any quorum, in any order, must combine to the same point. This is the + * property that lets different participants show up in each signing round. */ +static void run_iceberg_vpss_quorum_test_internal(void) { + unsigned char all[7], quorum[7]; + secp256k1_ge points[7], chosen[7]; + secp256k1_scalar coeffs[3]; + secp256k1_gej expected, combined; + unsigned int t = 3, n = 7; + size_t i; + + if (!iceberg_test_fits(n)) { + return; + } + + for (i = 0; i < n; i++) { + all[i] = (unsigned char)(i + 1); + } + for (i = 0; i < t; i++) { + iceberg_test_random_scalar(&coeffs[i]); + } + iceberg_test_commitments(points, all, n, coeffs, t); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &expected, &coeffs[0]); + + for (i = 0; i < t; i++) { + quorum[i] = (unsigned char)(i + 1); + } + do { + for (i = 0; i < t; i++) { + chosen[i] = points[quorum[i] - 1]; + } + CHECK(secp256k1_vpss_combine_var(CTX, &combined, quorum, chosen, t) == 1); + { + secp256k1_ge got; + secp256k1_ge_set_gej(&got, &combined); + CHECK(secp256k1_gej_eq_ge_var(&expected, &got)); + } + /* The same quorum arranged backwards. next_combination only ever hands + * out ascending sets, so without this the "in any order" above would be + * an assumption: the weights are a function of the index set, not of how + * it is laid out in the array. */ + { + unsigned char reversed[7]; + secp256k1_ge backwards[7]; + secp256k1_ge got; + for (i = 0; i < t; i++) { + reversed[i] = quorum[t - 1 - i]; + backwards[i] = points[reversed[i] - 1]; + } + CHECK(secp256k1_vpss_combine_var(CTX, &combined, reversed, backwards, t) == 1); + secp256k1_ge_set_gej(&got, &combined); + CHECK(secp256k1_gej_eq_ge_var(&expected, &got)); + } + } while (iceberg_test_next_combination(quorum, t, n)); +} + +/* Deal a group and check the aggregated public key really is the shared secret. + * + * The secret is reconstructed here only to confirm the public path agrees with + * it. No participant ever computes this value, and the API offers no way to. */ +static void run_iceberg_keygen_test_internal(void) { + static const unsigned char configs[][2] = { {3,2}, {5,3}, {5,2}, {7,4}, {9,5}, {10,5} }; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share_cache caches[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned char seed[32]; + size_t config, i; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1]; + unsigned int mu = 2 * t - 1; + secp256k1_pubkey group_pk, again; + secp256k1_scalar secret_shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1]; + secp256k1_scalar recovered; + unsigned char quorum[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar quorum_vals[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_gej expected_gej; + secp256k1_ge expected, got; + unsigned int k; + + if (!iceberg_test_fits(n)) { + continue; + } + + testrand256(seed); + for (k = 1; k <= n; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, n, t, seed) == 1); + + for (k = 1; k <= n; k++) { + const unsigned char *seeds; + unsigned int gn, gt, gk; + size_t count; + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_SEEDS]; + + CHECK(secp256k1_iceberg_share_cache_create(CTX, &caches[k - 1], &shares[k - 1]) == 1); + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], &caches[k - 1]) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + + /* Reconstruct this participant's secret share, for the check below. */ + count = secp256k1_iceberg_share_load(CTX, &gn, >, &gk, &seeds, &shares[k - 1]); + CHECK(count > 0 && gn == n && gt == t && gk == k); + CHECK(secp256k1_rss_lagrange_weights_var(weights, n, t, k) == count); + secp256k1_rss_eval(secp256k1_get_hash_context(CTX), &secret_shares[k], seeds, weights, + count, secp256k1_iceberg_keygen_label, + sizeof(secp256k1_iceberg_keygen_label)); + } + + /* A quorum of exactly 2t-1 aggregates to the group key. */ + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, mu, n, t) == 1); + + /* And that key is the commitment to the reconstructed secret. */ + for (i = 0; i < t; i++) { + quorum[i] = (unsigned char)(i + 1); + quorum_vals[i] = secret_shares[i + 1]; + } + secp256k1_scalarpoly_interpolate_at0(&recovered, quorum, quorum_vals, t); + secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &expected_gej, &recovered); + secp256k1_ge_set_gej(&expected, &expected_gej); + CHECK(secp256k1_pubkey_load(CTX, &got, &group_pk)); + CHECK(secp256k1_ge_eq_var(&expected, &got)); + + /* The full group gives the same answer as the 2t-1 quorum. */ + if (n > mu) { + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &again, pubshare_ptrs, n, n, t) == 1); + CHECK(secp256k1_memcmp_var(&group_pk, &again, sizeof(group_pk)) == 0); + } + + /* Fewer than 2t-1 shares proves nothing and must be refused. */ + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &again, pubshare_ptrs, mu - 1, n, t) == 0); + + /* Recomputing the weights must match using the cache. Against a saved + * copy, not against pubshare_ptrs[0]: that points at pubshares[0], which + * this line overwrites. */ + { + secp256k1_iceberg_pubshare with_cache = pubshares[0]; + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[0], &shares[0], NULL) == 1); + CHECK(secp256k1_memcmp_var(&pubshares[0], &with_cache, sizeof(pubshares[0])) == 0); + } + } +} + +/* Serialization round-trips, and the configurations the scheme cannot express + * are rejected at the point of dealing rather than later. */ +static void run_iceberg_keygen_api_test(void) { + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share parsed; + secp256k1_iceberg_share_cache cache; + secp256k1_iceberg_pubshare pubshare, reparsed; + unsigned char seed[32], buf[4 + 32 * SECP256K1_ICEBERG_MAX_SEEDS], wire[34]; + size_t len, i; + + /* The only group this deals is five; the larger ones below are refusals. */ + if (!iceberg_test_fits(5)) { + return; + } + + memset(seed, 0x7c, sizeof(seed)); + for (i = 0; i < SECP256K1_ICEBERG_MAX_PARTICIPANTS; i++) { + ptrs[i] = &shares[i]; + } + + /* Minority threshold only: t <= (n+1)/2, because a quorum of 2t-1 has to + * fit in the group. These are the configurations people ask for first. */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, 2, 2, seed)); /* 2-of-2 */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, 4, 3, seed)); /* 3-of-4 */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, 6, 4, seed)); /* 4-of-6 */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, ptrs, SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1, 2, seed)); + + CHECK(secp256k1_iceberg_shares_gen(CTX, ptrs, 5, 3, seed) == 1); + + /* Share round-trip, and a serialized share is exactly as large as it needs + * to be rather than as large as the opaque type. */ + len = sizeof(buf); + CHECK(secp256k1_iceberg_share_serialize(CTX, buf, &len, &shares[2]) == 1); + CHECK(len == 4 + 32 * secp256k1_rss_binom(4, 2)); + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len) == 1); + CHECK(secp256k1_memcmp_var(&parsed, &shares[2], sizeof(parsed)) == 0); + + /* Truncated, over-long and wrong-version inputs are all refused. The + * over-long case matters on its own: a caller pulling a share out of a + * longer record and passing the whole buffer must not have the tail + * ignored. */ + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len - 1) == 0); + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len + 1) == 0); + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, 3) == 0); + buf[0] = 2; + CHECK(secp256k1_iceberg_share_parse(CTX, &parsed, buf, len) == 0); + buf[0] = 1; + + /* Too small a buffer reports the size it wanted. */ + len = 4; + CHECK(secp256k1_iceberg_share_serialize(CTX, buf, &len, &shares[2]) == 0); + CHECK(len == 4 + 32 * secp256k1_rss_binom(4, 2)); + + /* A malformed share has no length to report, so it reports zero. A caller + * that sizes a second call from *outlen, which is what the short-buffer case + * above teaches it to do, would otherwise loop on the stale value + * forever. */ + { + secp256k1_iceberg_share broken = shares[2]; + broken.data[0] ^= 1; + len = sizeof(buf); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_share_serialize(CTX, buf, &len, &broken)); + CHECK(len == 0); + } + + /* SECP256K1_ICEBERG_SHARE_MAX_LEN is what the header promises callers can + * size a buffer with, so it has to bound every configuration and not just + * the ones above. The per-configuration check stays even though the last + * line implies it: it fires on the offending n and t, where the other only + * says the maximum came out wrong. */ + { + unsigned int n, t; + size_t largest = 0; + for (n = 2; n <= SECP256K1_ICEBERG_MAX_PARTICIPANTS; n++) { + for (t = 1; 2 * t - 1 <= n; t++) { + size_t need = 4 + 32 * secp256k1_rss_binom(n - 1, t - 1); + CHECK(need <= SECP256K1_ICEBERG_SHARE_MAX_LEN); + if (need > largest) { + largest = need; + } + } + } + CHECK(largest == SECP256K1_ICEBERG_SHARE_MAX_LEN); + } + + /* A cache belonging to another participant must not be usable, since it + * would silently produce a wrong share rather than an error. */ + CHECK(secp256k1_iceberg_share_cache_create(CTX, &cache, &shares[0]) == 1); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_pubshare_gen(CTX, &pubshare, &shares[1], &cache)); + + /* Public share round-trip. */ + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshare, &shares[0], NULL) == 1); + CHECK(secp256k1_iceberg_pubshare_serialize(CTX, wire, &pubshare) == 1); + CHECK(secp256k1_iceberg_pubshare_parse(CTX, &reparsed, wire) == 1); + CHECK(secp256k1_memcmp_var(&reparsed, &pubshare, sizeof(pubshare)) == 0); + wire[0] = 0; + CHECK(secp256k1_iceberg_pubshare_parse(CTX, &reparsed, wire) == 0); +} + +/* The three objects that cross a network survive the trip, and the encodings + * that would be ambiguous or unusable on the far side are refused. */ +static void run_iceberg_wire_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubnonce nonces[N], parsed_nonce; + const secp256k1_iceberg_pubnonce *nonce_ptrs[N]; + secp256k1_iceberg_aggnonce aggnonce, parsed_agg; + secp256k1_iceberg_partial_sig psig, parsed_psig; + secp256k1_musig_pubnonce group_pubnonce; + secp256k1_pubkey group_pk; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N]; + unsigned char seed[32], sid[32]; + unsigned char nonce67[67], agg66[66], psig33[33]; + unsigned int k; + + if (!iceberg_test_fits(N)) { + return; + } + + memset(seed, 0x5e, sizeof(seed)); + memset(sid, 0x9a, sizeof(sid)); + for (k = 0; k < N; k++) { + share_ptrs[k] = &shares[k]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (k = 0; k < N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k], &shares[k], NULL) == 1); + pubshare_ptrs[k] = &pubshares[k]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + /* Any 32 bytes serve as a label: this test is about encodings, and the one + * object here that would need an outer session is built by hand below. */ + for (k = 0; k < N; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k], &shares[k], NULL, sid) == 1); + nonce_ptrs[k] = &nonces[k]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &aggnonce, + nonce_ptrs, MU, N, T, &group_pk) == 1); + + /* The internal aggregate is optional, since nothing in the API takes one + * back. Asking for it must not change the nonce that does get published. */ + { + secp256k1_musig_pubnonce without; + CHECK(secp256k1_iceberg_nonce_agg(CTX, &without, NULL, + nonce_ptrs, MU, N, T, &group_pk) == 1); + CHECK(secp256k1_memcmp_var(&without, &group_pubnonce, sizeof(without)) == 0); + } + + /* Round-trips. Comparing the opaque structs rather than re-serializing + * catches a parse that loses something the encoding did carry. */ + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, nonce67, &nonces[2]) == 1); + CHECK(nonce67[0] == 3); + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 1); + CHECK(secp256k1_memcmp_var(&parsed_nonce, &nonces[2], sizeof(parsed_nonce)) == 0); + + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, agg66, &aggnonce) == 1); + CHECK(secp256k1_iceberg_aggnonce_parse(CTX, &parsed_agg, agg66) == 1); + CHECK(secp256k1_memcmp_var(&parsed_agg, &aggnonce, sizeof(parsed_agg)) == 0); + + /* A signature share, built by hand from a known scalar rather than by + * signing, so this test needs no outer session. */ + { + unsigned char raw[33]; + memset(raw, 0, sizeof(raw)); + raw[0] = 4; + raw[32] = 0x2b; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &psig, raw) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, psig33, &psig) == 1); + CHECK(secp256k1_memcmp_var(psig33, raw, sizeof(raw)) == 0); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &parsed_psig, psig33) == 1); + CHECK(secp256k1_memcmp_var(&parsed_psig, &psig, sizeof(psig)) == 0); + } + + /* An index past the end of the group names nobody. Zero is worse than that + * and the two are worth separating: zero is where the secret lives. A set of + * contributions containing a node at 0 puts the interpolation on top of one + * of its own nodes, where the Lagrange weights are 1 for that node and 0 for + * every other, so whoever sent it would be handing the group its aggregate + * rather than contributing to it. These three comparisons are the only thing + * standing there. */ + nonce67[0] = 0; + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 0); + nonce67[0] = SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1; + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 0); + nonce67[0] = 3; + psig33[0] = 0; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &parsed_psig, psig33) == 0); + psig33[0] = 4; + + /* Not a point. Corrupting the x coordinate is no good as a test, since + * about half of all x values do have a point, so break the prefix byte, which + * is invalid for any x. */ + { + unsigned char prefix = nonce67[1]; + nonce67[1] = 0x04; + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &parsed_nonce, nonce67) == 0); + nonce67[1] = prefix; + } + agg66[33] = 0x04; + CHECK(secp256k1_iceberg_aggnonce_parse(CTX, &parsed_agg, agg66) == 0); + + /* A signature share at or above the group order. Aggregation is linear, so + * one that wrapped would combine into a signature that merely fails to + * verify, and nothing says which share was wrong. */ + memset(&psig33[1], 0xff, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &parsed_psig, psig33) == 0); + + /* Infinity is representable, and must be, because the participants choose + * their own contributions and nothing stops them summing to it. */ + memset(agg66, 0, sizeof(agg66)); + CHECK(secp256k1_iceberg_aggnonce_parse(CTX, &parsed_agg, agg66) == 1); + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, agg66, &parsed_agg) == 1); + for (k = 0; k < sizeof(agg66); k++) { + CHECK(agg66[k] == 0); + } +} + +/* Rebuild one participant's serialized share from the group's seeds. + * + * The vectors carry the seeds once rather than every share, because each seed + * belongs to n-(t-1) of them. Participant k holds the seeds whose subset leaves + * it out, in the same rank order the dealer writes them. */ +static size_t iceberg_test_share_from_seeds(unsigned char *out, const struct iceberg_session_vector *v, unsigned int k) { + size_t used = 4, rank; + + out[0] = 1; + out[1] = (unsigned char)v->n; + out[2] = (unsigned char)v->t; + out[3] = (unsigned char)k; + for (rank = 0; rank < v->n_seeds; rank++) { + secp256k1_rss_subset subset = secp256k1_rss_subset_unrank(v->n, v->t - 1, (uint32_t)rank); + if (subset & (secp256k1_rss_subset)(1u << k)) { + continue; + } + memcpy(&out[used], v->seeds[rank], 32); + used += 32; + } + CHECK(used == 4 + 32 * secp256k1_rss_binom(v->n - 1, v->t - 1)); + return used; +} + +/* Everything the group does, against what the Python reference produced. + * + * Until this existed the signing path was only ever checked against itself, so + * a systematic disagreement with the reference would have passed every other + * test in this file. It is not an authority, since both implementations + * could be wrong the same way, but it is what catches them drifting apart. */ +static void run_iceberg_vectors_test(void) { + size_t which; + + for (which = 0; which < sizeof(iceberg_session_vectors) / sizeof(iceberg_session_vectors[0]); which++) { + const struct iceberg_session_vector *v = &iceberg_session_vectors[which]; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubnonce nonces[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubnonce *nonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_partial_sig psigs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_partial_sig *psig_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_aggnonce aggnonce; + secp256k1_musig_pubnonce group_pubnonce, cosigner_pubnonce; + const secp256k1_musig_pubnonce *just_the_cosigner[1]; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_partial_sig group_psig; + secp256k1_pubkey group_pk, cosigner_pk, parsed_group_pk; + const secp256k1_pubkey *pubkeys[2]; + unsigned char buf[4 + 32 * SECP256K1_ICEBERG_MAX_SEEDS]; + unsigned char wire[67], sid[32]; + unsigned int k; + size_t len; + + if (!iceberg_test_fits(v->n)) { + continue; + } + + for (k = 1; k <= v->n; k++) { + len = iceberg_test_share_from_seeds(buf, v, k); + CHECK(secp256k1_iceberg_share_parse(CTX, &shares[k - 1], buf, len) == 1); + } + + /* Public shares. Each one is an independent evaluation of the keygen + * label, so a disagreement here is in the PRF, the Lagrange weights or + * the seed layout, before any of the session machinery runs. */ + for (k = 1; k <= v->n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + CHECK(secp256k1_iceberg_pubshare_serialize(CTX, wire, &pubshares[k - 1]) == 1); + CHECK(wire[0] == k); + CHECK(secp256k1_memcmp_var(&wire[1], v->pubshares[k - 1], 33) == 0); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, v->mu, v->n, v->t) == 1); + + CHECK(secp256k1_ec_pubkey_parse(CTX, &parsed_group_pk, v->group_pk, 33) == 1); + CHECK(secp256k1_memcmp_var(&group_pk, &parsed_group_pk, sizeof(group_pk)) == 0); + + /* The outer session, whose key and nonce the vector fixes. */ + CHECK(secp256k1_ec_pubkey_parse(CTX, &cosigner_pk, v->cosigner_pk, 33) == 1); + pubkeys[0] = &group_pk; + pubkeys[1] = &cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, NULL, &keyagg_cache, pubkeys, 2) == 1); + CHECK(secp256k1_musig_pubnonce_parse(CTX, &cosigner_pubnonce, v->cosigner_pubnonce) == 1); + just_the_cosigner[0] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &cosigner_aggnonce, just_the_cosigner, 1) == 1); + + /* The label is an input, so the vector carries it. Any 32 bytes would + * do; these are the ones the generator happened to choose. */ + memcpy(sid, v->sid, 32); + + /* Round one. */ + for (k = 1; k <= v->mu; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, wire, &nonces[k - 1]) == 1); + CHECK(wire[0] == k); + CHECK(secp256k1_memcmp_var(&wire[1], v->pubnonces[k - 1], 66) == 0); + nonce_ptrs[k - 1] = &nonces[k - 1]; + } + + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &aggnonce, + nonce_ptrs, v->mu, v->n, v->t, &group_pk) == 1); + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, wire, &aggnonce) == 1); + CHECK(secp256k1_memcmp_var(wire, v->group_aggnonce, 66) == 0); + /* The published nonce differs from the internal one by b1: the + * aggregate compared above does not cover the nesting coefficient and + * this does. So do the signature shares below, by a different route. */ + CHECK(secp256k1_musig_pubnonce_serialize(CTX, wire, &group_pubnonce) == 1); + CHECK(secp256k1_memcmp_var(wire, v->group_pubnonce, 66) == 0); + + /* Round two. */ + for (k = 1; k <= v->t; k++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[k - 1], &shares[k - 1], NULL, sid, nonce_ptrs, v->mu, &group_pk, &keyagg_cache, v->msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, wire, &psigs[k - 1]) == 1); + CHECK(wire[0] == k); + CHECK(secp256k1_memcmp_var(&wire[1], v->psigs[k - 1], 32) == 0); + psig_ptrs[k - 1] = &psigs[k - 1]; + + /* Verify the vector's share, parsed back off the wire, rather than + * the one this run just produced. Everywhere else the verifier is + * checked against another run of the code that wrote it. */ + { + secp256k1_iceberg_partial_sig from_wire; + unsigned char in33[33]; + + in33[0] = (unsigned char)k; + memcpy(&in33[1], v->psigs[k - 1], 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &from_wire, in33) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &from_wire, &pubshares[k - 1], + nonce_ptrs, v->mu, v->n, v->t, &group_pk, &keyagg_cache, + v->msg, &cosigner_aggnonce) == 1); + } + } + + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, v->t, v->n, v->t) == 1); + CHECK(secp256k1_musig_partial_sig_serialize(CTX, wire, &group_psig) == 1); + CHECK(secp256k1_memcmp_var(wire, v->group_psig, 32) == 0); + } +} + +/* A participant who publishes something other than its real commitment raises + * the degree of the interpolant and is caught. */ +static void run_iceberg_keygen_cheat_test(void) { + secp256k1_iceberg_share shares[5]; + secp256k1_iceberg_share *ptrs[5]; + secp256k1_iceberg_pubshare pubshares[5]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[5]; + secp256k1_pubkey group_pk; + unsigned char seed[32]; + unsigned int k; + + testrand256(seed); + for (k = 0; k < 5; k++) { + ptrs[k] = &shares[k]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, ptrs, 5, 3, seed) == 1); + for (k = 0; k < 5; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k], &shares[k], NULL) == 1); + pubshare_ptrs[k] = &pubshares[k]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 5, 5, 3) == 1); + + /* Swap two participants' commitments: each is individually valid, but they + * no longer lie on one polynomial. */ + { + secp256k1_iceberg_pubshare swapped = pubshares[1]; + pubshares[1] = pubshares[3]; + pubshares[3] = swapped; + /* Restore the indices so only the points are wrong. */ + pubshares[1].data[4] = 2; + pubshares[3].data[4] = 4; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 5, 5, 3) == 0); + + /* Duplicated indices are refused rather than producing a singular system. */ + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[1], &shares[1], NULL) == 1); + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[3], &shares[3], NULL) == 1); + pubshares[3].data[4] = 2; + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 5, 5, 3) == 0); +} + +/* The three parsers, against input nobody meant them to see. + * + * The existing wire test picks malformations by hand: index zero, index past + * the maximum, a point off the curve, a scalar at the group order. Those are the + * cases somebody thought of. This covers the space between them: random buffers, + * and single-bit flips of valid encodings, which is where a parser that is + * almost right tends to be wrong. + * + * The invariant is round-tripping rather than acceptance. A parser is free to + * reject anything it likes; what it must not do is accept something it cannot + * then re-emit, because that is a value the rest of the module will treat as + * well-formed and the wire will not agree about. + */ +static void run_iceberg_parser_fuzz_test(void) { + secp256k1_iceberg_share shares[3]; + secp256k1_iceberg_share *share_ptrs[3]; + secp256k1_iceberg_pubnonce pubnonce, reparsed_nonce; + secp256k1_iceberg_aggnonce aggnonce, reparsed_agg; + secp256k1_iceberg_partial_sig psig, reparsed_psig; + secp256k1_musig_pubnonce musig_pubnonce; + const secp256k1_iceberg_pubnonce *nonce_ptrs[3]; + secp256k1_iceberg_pubshare pubshares[3]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[3]; + secp256k1_pubkey group_pk; + unsigned char seed[32], sid[32], seen[3][67], buffer[67], again[67]; + unsigned int k; + int i, round; + + testrand256(seed); + testrand256(sid); + for (k = 0; k < 3; k++) { + share_ptrs[k] = &shares[k]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, 3, 2, seed) == 1); + for (k = 0; k < 3; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k], &shares[k], NULL) == 1); + pubshare_ptrs[k] = &pubshares[k]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, 3, 3, 2) == 1); + + /* One real encoding of each, to flip bits in. */ + CHECK(secp256k1_iceberg_nonce_gen(CTX, &pubnonce, &shares[0], NULL, sid) == 1); + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, seen[0], &pubnonce) == 1); + + /* Three distinct objects: nonce_agg reads each pointer in turn, so three + * pointers at one object would offer the same participant's index three + * times and be refused as duplicates. */ + { + secp256k1_iceberg_pubnonce nonces[3]; + for (k = 0; k < 3; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k], &shares[k], NULL, sid) == 1); + nonce_ptrs[k] = &nonces[k]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, &aggnonce, + nonce_ptrs, 3, 3, 2, &group_pk) == 1); + } + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, seen[1], &aggnonce) == 1); + memset(&psig, 0, sizeof(psig)); + { + /* A signature share built by hand, so this test does not need a whole + * session to have something well-formed to corrupt. */ + unsigned char raw[33]; + memset(raw, 0, sizeof(raw)); + raw[0] = 1; + /* Just under the group order, so a flipped bit in the top half has a + * chance of pushing it over rather than always landing in range. */ + raw[1] = 0xff; raw[2] = 0xff; raw[3] = 0xff; raw[4] = 0xff; + raw[32] = 7; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &psig, raw) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, seen[2], &psig) == 1); + } + + /* Random bytes will not find the scalar boundary: a random 32-byte value + * exceeds the group order with probability about 2^-128, so it essentially + * never overflows, and neither does a bit flipped in a small one. The out-of-range share is + * therefore named explicitly below. Without it a parser that reduced + * instead of rejecting passes everything above. */ + { + static const unsigned char order[32] = { + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe, + 0xba,0xae,0xdc,0xe6,0xaf,0x48,0xa0,0x3b,0xbf,0xd2,0x5e,0x8c,0xd0,0x36,0x41,0x41 + }; + unsigned char probe[33]; + probe[0] = 1; + + memcpy(&probe[1], order, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 0); + + memset(&probe[1], 0xff, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 0); + + /* One below the order is the largest thing that must be accepted, and it + * has to survive a round trip unchanged rather than being reduced. */ + memcpy(&probe[1], order, 32); + probe[32] -= 1; + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, again, &reparsed_psig) == 1); + CHECK(secp256k1_memcmp_var(again, probe, 33) == 0); + + /* And zero, which is in range and easy to special-case by accident. */ + memset(&probe[1], 0, 32); + CHECK(secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, probe) == 1); + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, again, &reparsed_psig) == 1); + CHECK(secp256k1_memcmp_var(again, probe, 33) == 0); + } + + /* round 0: random bytes. round 1: one bit flipped in something valid. */ + for (round = 0; round < 2; round++) { + for (i = 0; i < 64 * COUNT; i++) { + int which = (int)testrand_int(3); + size_t length = which == 0 ? 67 : (which == 1 ? 66 : 33); + + if (round == 0) { + testrand_bytes_test(buffer, length); + } else { + memcpy(buffer, seen[which], length); + testrand_flip(buffer, length); + } + + if (which == 0) { + if (secp256k1_iceberg_pubnonce_parse(CTX, &reparsed_nonce, buffer)) { + CHECK(secp256k1_iceberg_pubnonce_serialize(CTX, again, &reparsed_nonce) == 1); + CHECK(secp256k1_memcmp_var(again, buffer, length) == 0); + } + } else if (which == 1) { + if (secp256k1_iceberg_aggnonce_parse(CTX, &reparsed_agg, buffer)) { + CHECK(secp256k1_iceberg_aggnonce_serialize(CTX, again, &reparsed_agg) == 1); + CHECK(secp256k1_memcmp_var(again, buffer, length) == 0); + } + } else { + if (secp256k1_iceberg_partial_sig_parse(CTX, &reparsed_psig, buffer)) { + CHECK(secp256k1_iceberg_partial_sig_serialize(CTX, again, &reparsed_psig) == 1); + CHECK(secp256k1_memcmp_var(again, buffer, length) == 0); + } + } + } + } +} + +/* Constraint C1, exhaustively: whoever shows up, the signature is the same one. + * + * The scheme's whole reason for deriving nonces rather than storing them is that + * a member can go offline between the rounds and rejoin. So round one may be run + * by any 2t-1 members, round two signed by any t members, and the two sets need + * not overlap at all, and every combination must produce a byte-identical + * signature, because the aggregate nonce and the key are the same either way. + * + * The e2e test checks two of those combinations. This checks all of them, at + * sizes where "all of them" is cheap, because an earlier version of this module + * imposed a restriction that broke C1 and no test could see it: every + * configuration then had n == 2t-1, so there was only ever one round-one set. + * + * The cosigner's nonce is regenerated from the same randomness inside the loop. + * That is not laziness. The final signature depends on it, so holding it fixed + * is what makes "byte-identical" the right assertion rather than a coincidence. + */ +static void run_iceberg_c1_property_test(void) { + static const unsigned char configs[][2] = { {4,2}, {5,2} }; + size_t config; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1], mu = 2 * t - 1; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubnonce nonces[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubnonce *nonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_partial_sig psigs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_partial_sig *psig_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + + secp256k1_pubkey group_pk, cosigner_pk; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + unsigned char seed[32], msg[32], seckey[32], secrand[32], sid[32]; + unsigned char first_sig[64]; + unsigned char round_one[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + /* musig_nonce_gen wipes the randomness it is handed, so that one call + * cannot silently become two. Keep a master copy and spend a fresh one + * each time round the loop. */ + unsigned char secrand_master[32]; + unsigned char signers[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned int k; + size_t i; + int have_first = 0; + int combinations = 0; + + if (!iceberg_test_fits(n)) { + continue; + } + + testrand256(seed); + testrand256(msg); + testrand256(seckey); + testrand256(secrand_master); + testrand256(sid); + + for (k = 1; k <= n; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, n, t, seed) == 1); + + for (k = 1; k <= n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, mu, n, t) == 1); + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + + /* Every set that could run round one. */ + for (i = 0; i < mu; i++) { + round_one[i] = (unsigned char)(i + 1); + } + do { + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + const secp256k1_musig_pubnonce *all_pubnonces[2]; + secp256k1_musig_aggnonce cosigner_aggnonce, full_aggnonce; + + for (i = 0; i < mu; i++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[i], + &shares[round_one[i] - 1], NULL, sid) == 1); + nonce_ptrs[i] = &nonces[i]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + nonce_ptrs, mu, n, t, &group_pk) == 1); + + /* Every set that could then sign, drawn from the whole group. */ + for (i = 0; i < t; i++) { + signers[i] = (unsigned char)(i + 1); + } + do { + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_session session; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + const secp256k1_musig_partial_sig *musig_psigs[2]; + unsigned char sig[64]; + + /* Rebuilt every time, from the same randomness, so the only + * thing varying across the loop is who took part. */ + memcpy(secrand, secrand_master, sizeof(secrand)); + CHECK(secp256k1_musig_nonce_gen(CTX, &cosigner_secnonce, &cosigner_pubnonce, + secrand, seckey, &cosigner_pk, msg, + &keyagg_cache, NULL) == 1); + all_pubnonces[0] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &cosigner_aggnonce, all_pubnonces, 1) == 1); + all_pubnonces[0] = &group_pubnonce; + all_pubnonces[1] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &full_aggnonce, all_pubnonces, 2) == 1); + CHECK(secp256k1_musig_nonce_process(CTX, &session, &full_aggnonce, + msg, &keyagg_cache, NULL) == 1); + + for (i = 0; i < t; i++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[signers[i] - 1], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &cosigner_secnonce, + &cosigner_keypair, &keyagg_cache, + &session) == 1); + musig_psigs[0] = &group_psig; + musig_psigs[1] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig, &session, musig_psigs, 2) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &agg_xonly) == 1); + + if (!have_first) { + memcpy(first_sig, sig, sizeof(sig)); + have_first = 1; + } else { + CHECK(secp256k1_memcmp_var(sig, first_sig, sizeof(sig)) == 0); + } + combinations++; + } while (iceberg_test_next_combination(signers, t, n)); + } while (iceberg_test_next_combination(round_one, mu, n)); + + /* C(n, mu) * C(n, t), so a silently truncated loop is caught. */ + CHECK(combinations == (int)(secp256k1_rss_binom(n, mu) * secp256k1_rss_binom(n, t))); + } +} + +/* A whole signing session: an Iceberg group standing in for one MuSig2 + * participant, beside an ordinary single signer who never learns a group is + * involved, ending in a BIP-340 signature. + * + * The cosigner produces its nonce first here, but only because something has to + * be written first. Both sides of round one need the session label and nothing + * from each other, so either order works and a group can open a session. + */ +static void run_iceberg_e2e_test_internal(void) { + /* The last two have n > 2t-1, so round one leaves somebody out. That is + * what reaches the branch below where a member absent from round one signs + * anyway; with n == 2t-1 throughout, nothing exercises it. */ + static const unsigned char configs[][2] = { {3,2}, {5,3}, {7,4}, {5,2}, {7,3} }; + size_t config; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + unsigned int n = configs[config][0], t = configs[config][1], mu = 2 * t - 1; + secp256k1_iceberg_share shares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share *share_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_share_cache first_cache; + secp256k1_iceberg_pubshare pubshares[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_pubnonce nonces[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubnonce *nonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_partial_sig psigs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_partial_sig *psig_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_aggnonce iceberg_aggnonce; + + secp256k1_pubkey group_pk, cosigner_pk; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + const secp256k1_musig_pubnonce *all_pubnonces[2]; + secp256k1_musig_aggnonce cosigner_aggnonce, full_aggnonce; + secp256k1_musig_session session; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + const secp256k1_musig_partial_sig *musig_psigs[2]; + + unsigned char seed[32], msg[32], seckey[32], secrand[32], sid[32]; + unsigned char sig[64], sig_again[64]; + unsigned int k; + size_t i; + + testrand256(seed); + testrand256(msg); + testrand256(seckey); + if (!iceberg_test_fits(n)) { + continue; + } + + testrand256(secrand); + + /** setup **/ + for (k = 1; k <= n; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, n, t, seed) == 1); + + for (k = 1; k <= n; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, mu, n, t) == 1); + + /* The group's key enters aggregation as an ordinary public key. */ + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + + /** round 1 **/ + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + + /* Any 32 bytes. The caller owns this choice and the rule about it. */ + memset(sid, 0x5b, sizeof(sid)); + + for (k = 1; k <= n; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + nonce_ptrs[k - 1] = &nonces[k - 1]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &iceberg_aggnonce, + nonce_ptrs, mu, n, t, &group_pk) == 1); + + all_pubnonces[0] = &group_pubnonce; + all_pubnonces[1] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &full_aggnonce, all_pubnonces, 2) == 1); + CHECK(secp256k1_musig_nonce_process(CTX, &session, &full_aggnonce, msg, &keyagg_cache, NULL) == 1); + + /** round 2 **/ + for (k = 1; k <= t; k++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[k - 1], &shares[k - 1], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[k - 1] = &psigs[k - 1]; + } + /* The cache must not change a single answer. It only saves recomputing + * the Lagrange weights, so the whole risk in it is that it silently + * produces a different one, and every other call in this file passes NULL, + * which would leave that undetected. Both rounds, byte for byte. */ + CHECK(secp256k1_iceberg_share_cache_create(CTX, &first_cache, &shares[0]) == 1); + { + secp256k1_iceberg_pubnonce cached_nonce; + secp256k1_iceberg_partial_sig cached_psig; + CHECK(secp256k1_iceberg_nonce_gen(CTX, &cached_nonce, &shares[0], &first_cache, sid) == 1); + CHECK(secp256k1_memcmp_var(&cached_nonce, &nonces[0], sizeof(cached_nonce)) == 0); + CHECK(secp256k1_iceberg_partial_sign(CTX, &cached_psig, &shares[0], &first_cache, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_memcmp_var(&cached_psig, &psigs[0], sizeof(cached_psig)) == 0); + } + + /* Every share verifies against its author's public share, at every + * configuration this test runs. */ + for (k = 1; k <= t; k++) { + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[k - 1], &pubshares[k - 1], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 1); + } + { + secp256k1_iceberg_partial_sig altered = psigs[0]; + unsigned char other_msg[32]; + + altered.data[10] ^= 0x40; + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &altered, &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + + /* Participant 2's share against participant 1's public share. */ + if (t >= 2) { + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[1], &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + } + + /* Participant 1's own share, relabeled as + * participant 2's, against participant 1's public share. Everything + * the equation touches is participant 1's, so it still balances, + * and nothing but the index comparison rejects this. Without that + * comparison the answer would be a confident yes to a question + * about participant 2. */ + altered = psigs[0]; + altered.data[4] = 2; + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &altered, &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + + /* An input that differs from the signer's gives the same 0 as a bad + * share, which is the reason a 0 is not evidence about a member. */ + memcpy(other_msg, msg, 32); + other_msg[0] ^= 1; + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, other_msg, + &cosigner_aggnonce) == 0); + + /* An uninitialized share is a 0 rather than a crash. */ + memset(&altered, 0, sizeof(altered)); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &altered, &pubshares[0], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + } + + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + + /* One share past the threshold, which is where aggregation can tell that + * a set contradicts itself. At exactly t it cannot: t points always lie + * on some polynomial of degree t-1, so the tampered share below is + * accepted there and refused here, and that difference is the check. */ + if (t + 1 <= n) { + secp256k1_musig_partial_sig with_spare; + secp256k1_iceberg_partial_sig tampered; + + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[t], &shares[t], NULL, sid, + nonce_ptrs, mu, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + psig_ptrs[t] = &psigs[t]; + + /* A spare share changes nothing: any qualifying set interpolates to + * the same value, which is the property the whole scheme rests on. */ + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &with_spare, psig_ptrs, t + 1, n, t) == 1); + CHECK(secp256k1_memcmp_var(&with_spare, &group_psig, sizeof(with_spare)) == 0); + + tampered = psigs[0]; + tampered.data[36] ^= 1; + psig_ptrs[0] = &tampered; + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &with_spare, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &with_spare, psig_ptrs, t + 1, n, t) == 0); + psig_ptrs[0] = &psigs[0]; + } + + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &cosigner_secnonce, + &cosigner_keypair, &keyagg_cache, &session) == 1); + + musig_psigs[0] = &group_psig; + musig_psigs[1] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig, &session, musig_psigs, 2) == 1); + + /* The whole point: an ordinary BIP-340 signature. */ + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &agg_xonly) == 1); + + /* A different quorum in round two produces the identical signature. + * The signers may be any t of the n. A member absent from round one + * still holds the share that fixes what its contribution would have + * been, so it can check the set it is handed and sign against it. The + * case below with mu < n exercises exactly that. */ + if (mu > t) { + for (i = 0; i < t; i++) { + k = (unsigned int)(mu - t + i + 1); + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[k - 1], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig_again, &session, musig_psigs, 2) == 1); + CHECK(secp256k1_memcmp_var(sig, sig_again, 64) == 0); + CHECK(secp256k1_schnorrsig_verify(CTX, sig_again, msg, 32, &agg_xonly) == 1); + } + + /* What the module enforces, and what it does not. + * + * It does NOT refuse a different message under the same label. The + * label is an argument again, because Lightning fixes the nonce a full + * round-trip before the transaction exists and a label derived from the + * message could not be computed in time. So one label with two messages + * is expressible, it is the first attack in this family, and it is the + * caller's job to make it impossible: half by never answering twice + * under one label, half by the group agreeing which message a label + * belongs to. See doc/iceberg.md. + * + * What it does enforce is that the contributions it is handed belong to + * the session it thinks it is in. */ + { + /* A set from a different label is refused. The contributions are + * internally consistent, since they are a real sharing, but the + * polynomial they define does not pass through this participant's + * own contribution for the label it is signing under, and that is + * what the check compares. */ + secp256k1_iceberg_pubnonce elsewhere[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + const secp256k1_iceberg_pubnonce *elsewhere_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + unsigned char other_sid[32]; + memcpy(other_sid, sid, 32); + other_sid[0] ^= 1; + for (i = 0; i < mu; i++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &elsewhere[i], &shares[i], NULL, other_sid) == 1); + elsewhere_ptrs[i] = &elsewhere[i]; + } + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, elsewhere_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 0); + + /* And the property that check must not cost: a member who was + * absent from round one can still sign. Its contribution is a + * function of its share and the label, so it can derive what its + * contribution would have been and compare that against the + * polynomial, without having been one of the contributors. + * + * Requiring the member's own contribution to be present would break + * this and buy nothing: a corrupt minority can fit a consistent set + * through any single honest contribution either way. Only checkable + * when the group is larger than the quorum. */ + if (n > mu) { + const secp256k1_iceberg_pubnonce *without_us[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + for (i = 0; i < mu; i++) { + without_us[i] = nonce_ptrs[i + 1]; + } + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, without_us, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[mu], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + + /* And that share verifies, which is the case the verifier exists + * for: participant mu+1 published no contribution, so + * there is no nonce of its own to hand over. The set fixes what + * its nonce had to be, and the verifier reads it from there. */ + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[mu], + nonce_ptrs, mu, n, t, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 1); + } + + /* The count a caller gets wrong: t contributions instead of the + * 2t-1 this needs, because t is the number that signs and it is + * easy to assume it is also the number to pass. Returns 0 rather + * than aborting, which is what makes it worth documenting as + * recoverable. */ + if (mu > t) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, nonce_ptrs, t, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 0); + } + + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[0], &shares[0], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + for (i = 1; i < t; i++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[i], NULL, sid, nonce_ptrs, mu, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + psig_ptrs[0] = &psigs[0]; + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, t, n, t) == 1); + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig_again, &session, musig_psigs, 2) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig_again, msg, 32, &agg_xonly) == 1); + } + } +} + +/* Iceberg under tweaks, which every Taproot output uses. + * + * `tacc` belongs to the outer session: musig_nonce_process sets `e * tacc` + * aside and musig_partial_sig_agg adds it once. A group's shares therefore + * carry `e * a * g * gacc` and NOT the tweak, or it is added twice and the + * signature fails. Nothing in the module mentions tacc, so the correct + * behavior is an absence, and this is what would catch someone reintroducing + * it while editing the key coefficient. + * + * Both tweak kinds and both orders, since tacc accumulates and an x-only tweak + * can flip the aggregate key's parity. */ +static void run_iceberg_tweak_test_internal(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + /* ec then xonly, xonly then ec, two xonly, and one of each alone. */ + static const unsigned char plans[][2] = { {0,1}, {1,0}, {1,1}, {0,2}, {1,2} }; + size_t plan; + + if (!iceberg_test_fits(N)) { + return; + } + + for (plan = 0; plan < sizeof(plans) / sizeof(plans[0]); plan++) { + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N]; + secp256k1_iceberg_pubnonce nonces[N]; + const secp256k1_iceberg_pubnonce *nonce_ptrs[N]; + secp256k1_iceberg_partial_sig psigs[N]; + const secp256k1_iceberg_partial_sig *psig_ptrs[N]; + secp256k1_iceberg_aggnonce iceberg_aggnonce; + + secp256k1_pubkey group_pk, cosigner_pk, output_pk; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly, output_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + const secp256k1_musig_pubnonce *all_pubnonces[2]; + secp256k1_musig_aggnonce cosigner_aggnonce, full_aggnonce; + secp256k1_musig_session session; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + const secp256k1_musig_partial_sig *musig_psigs[2]; + + unsigned char seed[32], msg[32], seckey[32], secrand[32], sid[32]; + unsigned char tweak[32], sig[64]; + unsigned int k, step; + + testrand256(seed); + testrand256(msg); + testrand256(seckey); + testrand256(secrand); + + for (k = 1; k <= N; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + pubshare_ptrs[k - 1] = &pubshares[k - 1]; + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + + /* Tweak the OUTER aggregate, which is where a Taproot output comes + * from. The group never learns it happened: nothing below this point + * passes the tweak to an Iceberg call. */ + for (step = 0; step < 2; step++) { + unsigned char kind = plans[plan][step]; + if (kind == 2) { + continue; /* this plan applies only one tweak */ + } + testrand256(tweak); + if (kind == 0) { + CHECK(secp256k1_musig_pubkey_ec_tweak_add(CTX, &output_pk, &keyagg_cache, tweak) == 1); + } else { + CHECK(secp256k1_musig_pubkey_xonly_tweak_add(CTX, &output_pk, &keyagg_cache, tweak) == 1); + } + } + CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &output_xonly, NULL, &output_pk) == 1); + + /** round 1, exactly as untweaked **/ + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + memset(sid, 0x3e, sizeof(sid)); + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + nonce_ptrs[k - 1] = &nonces[k - 1]; + } + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &iceberg_aggnonce, + nonce_ptrs, MU, N, T, &group_pk) == 1); + all_pubnonces[0] = &group_pubnonce; + all_pubnonces[1] = &cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &full_aggnonce, all_pubnonces, 2) == 1); + CHECK(secp256k1_musig_nonce_process(CTX, &session, &full_aggnonce, msg, &keyagg_cache, NULL) == 1); + + /** round 2 **/ + for (k = 1; k <= T; k++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[k - 1], &shares[k - 1], NULL, sid, nonce_ptrs, MU, &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 1); + psig_ptrs[k - 1] = &psigs[k - 1]; + } + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &group_psig, psig_ptrs, T, N, T) == 1); + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &cosigner_secnonce, + &cosigner_keypair, &keyagg_cache, &session) == 1); + musig_psigs[0] = &group_psig; + musig_psigs[1] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig, &session, musig_psigs, 2) == 1); + + /* Verifies under the TWEAKED key, and not under the untweaked one -- + * the second half matters, because a signature that verified under both + * would mean the tweak had not reached the challenge at all. */ + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &output_xonly) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig, msg, 32, &agg_xonly) == 0); + } +} + +REPEAT_TEST(run_iceberg_tweak_test) +REPEAT_TEST(run_iceberg_e2e_test) +REPEAT_TEST(run_iceberg_interpolate_test) +REPEAT_TEST(run_iceberg_batch_inverse_test) +REPEAT_TEST(run_iceberg_pss_test) +REPEAT_TEST(run_iceberg_vpss_test) +REPEAT_TEST(run_iceberg_vpss_quorum_test) +REPEAT_TEST(run_iceberg_keygen_test) + +/* The dealer's output, pinned to a fixed answer. + * + * Nothing else pins it. run_iceberg_vectors_test never calls shares_gen; it + * rebuilds shares from the per-subset seeds the vectors already carry, and + * every other test is self-consistent, so a change to the derivation moves all + * of them together and none of them notices. Without this test, changing the t + * byte of the dealer's preimage to t + 1 is caught by nothing. + * + * The digest is over the five serialized shares of one deal, in participant + * order. Regenerating it after a deliberate change is easy and almost always the + * wrong thing to do: a serialized share is a wire format two implementations + * have to agree on byte for byte, so a change here is a change to that format + * and the reference implementation has to move with it. */ +static void run_iceberg_dealer_kat_test(void) { + enum { N = 5, T = 3 }; + static const unsigned char expected[32] = { + 0x74, 0xf0, 0xec, 0x3f, 0xec, 0xbf, 0x47, 0xae, + 0x81, 0xbf, 0xec, 0x45, 0xdd, 0x01, 0xe2, 0x12, + 0xf5, 0x30, 0xd1, 0x1c, 0xf4, 0x7e, 0x89, 0x1a, + 0x7d, 0x19, 0xb5, 0x14, 0x71, 0xb8, 0xed, 0x09 + }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *ptrs[N]; + secp256k1_sha256 sha; + unsigned char seed[32], buf[SECP256K1_ICEBERG_SHARE_MAX_LEN], digest[32]; + unsigned int k; + size_t len; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0xa7, sizeof(seed)); + for (k = 1; k <= N; k++) { + ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, ptrs, N, T, seed) == 1); + + secp256k1_sha256_initialize(&sha); + for (k = 1; k <= N; k++) { + len = sizeof(buf); + CHECK(secp256k1_iceberg_share_serialize(CTX, buf, &len, &shares[k - 1]) == 1); + CHECK(len == 4 + 32 * secp256k1_rss_binom(N - 1, T - 1)); + /* The header gets its own assertion: a wrong version, group, threshold + * or index gives a clearer failure here than a digest ever will. */ + CHECK(buf[0] == 1 && buf[1] == N && buf[2] == T && buf[3] == k); + secp256k1_sha256_write(secp256k1_get_hash_context(CTX), &sha, buf, len); + } + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &sha, digest); + CHECK(secp256k1_memcmp_var(digest, expected, sizeof(digest)) == 0); +} + +/* An index the group does not contain must be refused by whatever knows the + * group size. + * + * Each check must bound against the group's own n, not the compile-time + * maximum. The parsers let the band n < k <= 10 through deliberately, having no + * way to know n, which leaves these calls as the only place it can be stopped. + * + * One group of ten is dealt and then five of its members, one of them + * participant 7, are presented to calls told the group is five. Dealing a single + * group is what makes this a test of the index check and not of the degree + * check: every point really does lie on the same degree t-1 polynomial, so a + * build that bounds by the maximum finds nothing else to object to and hands + * back the ten-member group's key to a caller who believes it has five. + * + * Each rejection is paired with the same call at n = 10, which must succeed. A + * test that only checks for 0 cannot tell a working bound from a broken set. */ +static void run_iceberg_foreign_index_test(void) { + enum { N = 10, T = 3, MU = 2 * T - 1, CLAIMED = 5 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *quorum_pubshares[MU]; + secp256k1_iceberg_pubnonce nonces[N]; + const secp256k1_iceberg_pubnonce *quorum_nonces[MU], *inside_nonces[MU]; + secp256k1_iceberg_partial_sig psigs[T]; + const secp256k1_iceberg_partial_sig *psig_ptrs[T]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, group_pubnonce; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_partial_sig musig_psig; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_pubkey group_pk, again, cosigner_pk; + unsigned char seed[32], seckey[32], secrand[32], sid[32], msg[32]; + /* Four members the claimed group of five contains, and one it does not. */ + const unsigned int members[MU] = { 1, 2, 3, 4, 7 }; + /* The t who go on to sign, participant 7 among them. */ + const unsigned int signers[T] = { 1, 2, 7 }; + /* A second quorum, every member of which the claimed group does contain. */ + const unsigned int inside[MU] = { 1, 2, 3, 4, 5 }; + unsigned int k; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x41, sizeof(seed)); + memset(seckey, 0x31, sizeof(seckey)); + memset(secrand, 0x77, sizeof(secrand)); + memset(sid, 0x53, sizeof(sid)); + memset(msg, 0x64, sizeof(msg)); + + for (k = 1; k <= N; k++) { + share_ptrs[k - 1] = &shares[k - 1]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[k - 1], &shares[k - 1], NULL) == 1); + } + for (i = 0; i < MU; i++) { + quorum_pubshares[i] = &pubshares[members[i] - 1]; + } + + /* Aggregation. The same five public shares, the same degree check. */ + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, quorum_pubshares, MU, N, T) == 1); + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &again, quorum_pubshares, MU, CLAIMED, T) == 0); + + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + + for (k = 1; k <= N; k++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &nonces[k - 1], &shares[k - 1], NULL, sid) == 1); + } + for (i = 0; i < MU; i++) { + quorum_nonces[i] = &nonces[members[i] - 1]; + inside_nonces[i] = &nonces[inside[i] - 1]; + } + + /* Round one. */ + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + quorum_nonces, MU, N, T, &group_pk) == 1); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + quorum_nonces, MU, CLAIMED, T, &group_pk) == 0); + + for (i = 0; i < T; i++) { + k = signers[i]; + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[k - 1], NULL, sid, + quorum_nonces, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + + /* Verification refuses twice over, and for two different reasons: first on + * an index inside the set of contributions, then, with a quorum the claimed + * group does contain, on the public share's own index. */ + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[signers[0] - 1], + quorum_nonces, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[signers[0] - 1], + quorum_nonces, MU, CLAIMED, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 0); + /* And participant 7's own share, checked against participant 7, over a + * quorum the claimed group does contain, so that the contribution indices + * cannot be what rejects it and the public share's own index is left as the + * only thing that can. Participant 7 can sign over a set it is absent from: + * that is the property partial_sign is built around. */ + { + secp256k1_iceberg_partial_sig seven; + CHECK(secp256k1_iceberg_partial_sign(CTX, &seven, &shares[6], NULL, sid, + inside_nonces, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &seven, &pubshares[6], + inside_nonces, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 1); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &seven, &pubshares[6], + inside_nonces, MU, CLAIMED, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 0); + } + + /* And aggregation of the signature shares, the last place it could get in. */ + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T, N, T) == 1); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T, CLAIMED, T) == 0); + + /* An index can also be wrong by being repeated, which makes the + * interpolation singular and is how a caller most easily passes fewer + * distinct participants than it believes it has. Every call that walks a set + * of indices has to catch it; only pubkey_agg was tested for it. */ + { + const secp256k1_iceberg_pubnonce *repeated[MU]; + const secp256k1_iceberg_partial_sig *repeated_psigs[T]; + + for (i = 0; i < MU; i++) { + repeated[i] = quorum_nonces[i]; + } + repeated[MU - 1] = repeated[0]; + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, NULL, + repeated, MU, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[signers[0] - 1], + repeated, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce) == 0); + for (i = 0; i < T; i++) { + repeated_psigs[i] = psig_ptrs[i]; + } + repeated_psigs[T - 1] = repeated_psigs[0]; + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, repeated_psigs, T, N, T) == 0); + } +} + +/* Every contribution at infinity, which is the only way to reach nonce_agg's + * last refusal. The degree check passes, because the zero polynomial has every + * degree; the group's pair then comes out as the point at infinity, which a + * MuSig2 public nonce has no encoding for. + * + * Which makes this the place to say what a refused call leaves behind. Nothing + * that loads: the aggnonce keeps the zero tag from the memset at the top of the + * call, so the serializer refuses it rather than handing back an aggregate the + * caller never got a 1 for. */ +static void run_iceberg_infinity_nonce_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_pubnonce nonces[MU]; + const secp256k1_iceberg_pubnonce *ptrs[MU]; + secp256k1_iceberg_aggnonce refused; + secp256k1_musig_pubnonce group_pubnonce; + secp256k1_pubkey group_pk; + unsigned char in67[67], out66[66], seckey[32]; + size_t i; + + memset(seckey, 0x9d, sizeof(seckey)); + CHECK(secp256k1_ec_pubkey_create(CTX, &group_pk, seckey) == 1); + + /* An all-zero point is how the 33-byte encoding spells infinity, so these + * parse rather than being refused one at a time. */ + memset(in67, 0, sizeof(in67)); + for (i = 0; i < MU; i++) { + in67[0] = (unsigned char)(i + 1); + CHECK(secp256k1_iceberg_pubnonce_parse(CTX, &nonces[i], in67) == 1); + ptrs[i] = &nonces[i]; + } + + memset(&refused, 0xab, sizeof(refused)); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &group_pubnonce, &refused, + ptrs, MU, N, T, &group_pk) == 0); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_aggnonce_serialize(CTX, out66, &refused)); +} + +/* Every documented bound that nothing else here reaches, at the severity the + * header documents for it. The distinction is the module's one departure from the + * library's convention and it is load-bearing: a count that arrives from a peer + * returns 0, and the group's own n and t abort, so a caller can tell "they sent + * me a bad set" from "I have a bug" without a debugger. + * + * The upper bounds are covered by run_iceberg_null_entry_test, which needs them + * to keep its own sweep unreachable. The lower bounds were covered nowhere, and + * neither was the static context, which three calls document and musig's own + * suite tests for its equivalents. */ +static void run_iceberg_bounds_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N], scratch_pubshare; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N + 1]; + secp256k1_iceberg_pubnonce pubnonces[N], scratch_pubnonce; + const secp256k1_iceberg_pubnonce *pubnonce_ptrs[N + 1]; + secp256k1_iceberg_partial_sig psigs[T], scratch_psig; + const secp256k1_iceberg_partial_sig *psig_ptrs[N + 1]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, musig_pubnonce; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_partial_sig musig_psig; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_pubkey group_pk, scratch_pk, cosigner_pk; + unsigned char seed[32], seckey[32], secrand[32], sid[32], msg[32]; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x5e, sizeof(seed)); + memset(seckey, 0x2b, sizeof(seckey)); + memset(secrand, 0x6f, sizeof(secrand)); + memset(sid, 0x3d, sizeof(sid)); + memset(msg, 0x71, sizeof(msg)); + for (i = 0; i < N; i++) { + share_ptrs[i] = &shares[i]; + pubshare_ptrs[i] = &pubshares[i]; + pubnonce_ptrs[i] = &pubnonces[i]; + } + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (i = 0; i < N; i++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[i], &shares[i], NULL) == 1); + CHECK(secp256k1_iceberg_nonce_gen(CTX, &pubnonces[i], &shares[i], NULL, sid) == 1); + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + for (i = 0; i < T; i++) { + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[i], NULL, sid, + pubnonce_ptrs, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + psig_ptrs[i] = &psigs[i]; + } + /* The entry past the group is null in each array, which is what makes the + * count bound the reason for the refusal. A duplicate there would be caught + * by the repeated-index check instead, and the bound could be deleted with + * the suite still passing; a null one reaches the sweep and aborts. */ + pubshare_ptrs[N] = NULL; + pubnonce_ptrs[N] = NULL; + for (i = T; i < N + 1; i++) { + psig_ptrs[i] = NULL; + } + + /* How many contributions turned up is a fact about the group, so every call + * that takes a count returns 0 rather than aborting: too few of them, and + * more of them than the group holds, which is one contribution arriving + * twice. */ + CHECK(secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, sid, + pubnonce_ptrs, MU - 1, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], pubnonce_ptrs, + MU - 1, N, T, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, MU - 1, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, N + 1, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, MU - 1, N, T) == 0); + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, N + 1, N, T) == 0); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T - 1, N, T) == 0); + CHECK(secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, N + 1, N, T) == 0); + + /* A threshold above (n+1)/2, which no group can have: 2t-1 would not fit. + * Every call that takes t checks it before the count, so each one can be + * asked in isolation. */ + CHECK_ILLEGAL(CTX, secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, + MU, N, (N + 1) / 2 + 1)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, MU, N, + (N + 1) / 2 + 1, &group_pk)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, + (N + 1) / 2 + 1, N, + (N + 1) / 2 + 1)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, (N + 1) / 2 + 1, seed)); + + /* The three calls whose Args line says "not secp256k1_context_static". Each + * multiplies by the generator, which that context has no table for. */ + CHECK_ILLEGAL(STATIC_CTX, secp256k1_iceberg_pubshare_gen(STATIC_CTX, &scratch_pubshare, + &shares[0], NULL)); + CHECK_ILLEGAL(STATIC_CTX, secp256k1_iceberg_nonce_gen(STATIC_CTX, &scratch_pubnonce, + &shares[0], NULL, sid)); + CHECK_ILLEGAL(STATIC_CTX, secp256k1_iceberg_partial_sign(STATIC_CTX, &scratch_psig, + &shares[0], NULL, sid, + pubnonce_ptrs, MU, &group_pk, + &keyagg_cache, msg, + &cosigner_aggnonce)); +} + +/* A null entry in a caller-supplied array. + * + * Every function here takes its contributions as an array of pointers, and a + * coordinator that assembles one from a map of who has answered can leave a + * hole in it. The sweep for null entries is the only thing that can object, so + * everything is built and left well formed before any entry is nulled: a + * zeroed object would be caught earlier by a magic-byte check inside a load + * helper instead. The failing calls write to scratch out-params because a call + * that fails still zeroes its own output first. */ +static void run_iceberg_null_entry_test(void) { + enum { N = 5, T = 3, MU = 2 * T - 1 }; + secp256k1_iceberg_share shares[N]; + secp256k1_iceberg_share *share_ptrs[N]; + secp256k1_iceberg_pubshare pubshares[N]; + const secp256k1_iceberg_pubshare *pubshare_ptrs[N]; + secp256k1_iceberg_pubnonce pubnonces[N]; + const secp256k1_iceberg_pubnonce *pubnonce_ptrs[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_iceberg_partial_sig psigs[T], scratch_psig; + const secp256k1_iceberg_partial_sig *psig_ptrs[T]; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_pubnonce cosigner_pubnonce, musig_pubnonce; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_partial_sig musig_psig; + secp256k1_keypair cosigner_keypair; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_pubkey group_pk, scratch_pk, cosigner_pk; + const secp256k1_pubkey *pubkeys[2]; + unsigned char seed[32], seckey[32], secrand[32], sid[32], msg[32]; + size_t i; + + if (!iceberg_test_fits(N)) { + return; + } + memset(seed, 0x7c, sizeof(seed)); + memset(seckey, 0x31, sizeof(seckey)); + memset(secrand, 0x77, sizeof(secrand)); + memset(sid, 0x11, sizeof(sid)); + memset(msg, 0x22, sizeof(msg)); + for (i = 0; i < N; i++) { + share_ptrs[i] = &shares[i]; + pubshare_ptrs[i] = &pubshares[i]; + pubnonce_ptrs[i] = &pubnonces[i]; + } + /* Past the group, left null on purpose: a count that reaches them is a + * count that should have been refused before the sweep ran. */ + for (i = N; i < SECP256K1_ICEBERG_MAX_PARTICIPANTS; i++) { + pubnonce_ptrs[i] = NULL; + } + for (i = 0; i < T; i++) { + psig_ptrs[i] = &psigs[i]; + } + + /* The dealer sweeps before it writes, so the real deal below is unaffected. */ + share_ptrs[2] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed)); + share_ptrs[2] = &shares[2]; + + CHECK(secp256k1_iceberg_shares_gen(CTX, share_ptrs, N, T, seed) == 1); + for (i = 0; i < N; i++) { + CHECK(secp256k1_iceberg_pubshare_gen(CTX, &pubshares[i], &shares[i], NULL) == 1); + } + CHECK(secp256k1_iceberg_pubkey_agg(CTX, &group_pk, pubshare_ptrs, MU, N, T) == 1); + + iceberg_test_cosigner_key(&cosigner_keypair, &cosigner_pk, &agg_xonly, &keyagg_cache, + &group_pk, seckey); + iceberg_test_cosigner_nonce(&cosigner_secnonce, &cosigner_pubnonce, &cosigner_aggnonce, + secrand, seckey, &cosigner_pk, msg, &keyagg_cache); + for (i = 0; i < N; i++) { + CHECK(secp256k1_iceberg_nonce_gen(CTX, &pubnonces[i], &shares[i], NULL, sid) == 1); + } + for (i = 0; i < T; i++) { + /* nonce_agg belongs in this sweep too: its count bound is what keeps the + * loop below inside an array the caller sized from n. */ + CHECK(secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, pubnonce_ptrs, + N + 1, N, T, &group_pk) == 0); + CHECK(secp256k1_iceberg_partial_sign(CTX, &psigs[i], &shares[i], NULL, sid, + pubnonce_ptrs, MU, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 1); + } + + pubshare_ptrs[2] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_pubkey_agg(CTX, &scratch_pk, pubshare_ptrs, MU, N, T)); + pubshare_ptrs[2] = &pubshares[2]; + + /* keyagg_check walks a list too, and its own is the cosigners' rather than + * the group's, so it needs its own turn. */ + pubkeys[1] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_keyagg_check(CTX, &keyagg_cache, pubkeys, 2, &group_pk)); + pubkeys[1] = &cosigner_pk; + + pubnonce_ptrs[2] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_nonce_agg(CTX, &musig_pubnonce, NULL, + pubnonce_ptrs, MU, N, T, &group_pk)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, + sid, pubnonce_ptrs, MU, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce)); + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], + pubnonce_ptrs, MU, N, T, &group_pk, + &keyagg_cache, msg, &cosigner_aggnonce)); + pubnonce_ptrs[2] = &pubnonces[2]; + + psig_ptrs[1] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_iceberg_partial_sig_agg(CTX, &musig_psig, psig_ptrs, T, N, T)); + psig_ptrs[1] = &psigs[1]; + + /* A count larger than the group is peer-influenced rather than a caller + * bug, so these return 0 instead. The distinction is what stops the sweep + * above from being reachable with a count it cannot walk. + * + * The first pair passes a count just past the group, with the entries past + * it null: that is the case the header's promise is about, and the one the + * sweep would otherwise reach first. The second pair passes a count past the + * compile-time bound too, which the same `> n` refuses: there is no + * separate check against MAX_PARTICIPANTS here, because n comes off the + * caller's own share and cannot exceed it. Both pairs are the one mechanism, + * approached from two distances. */ + CHECK(secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, sid, + pubnonce_ptrs, N + 1, &group_pk, &keyagg_cache, + msg, &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], pubnonce_ptrs, + N + 1, N, T, &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sign(CTX, &scratch_psig, &shares[0], NULL, sid, + pubnonce_ptrs, SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1, + &group_pk, &keyagg_cache, msg, &cosigner_aggnonce) == 0); + CHECK(secp256k1_iceberg_partial_sig_verify(CTX, &psigs[0], &pubshares[0], pubnonce_ptrs, + SECP256K1_ICEBERG_MAX_PARTICIPANTS + 1, N, T, + &group_pk, &keyagg_cache, msg, + &cosigner_aggnonce) == 0); +} + +static const struct tf_test_entry tests_iceberg[] = { + CASE1(run_iceberg_from_roots_test), + CASE1(run_iceberg_div_root_test), + CASE1(run_iceberg_lagrange_eval_test), + CASE1(run_iceberg_lagrange_basis_test), + CASE1(run_iceberg_interpolate_test), + CASE1(run_iceberg_batch_inverse_test), + CASE1(run_iceberg_binom_test), + CASE1(run_iceberg_subset_rank_test), + CASE1(run_iceberg_prf_test), + CASE1(run_iceberg_midstate_test), + CASE1(run_iceberg_keyagg_check_test), + CASE1(run_iceberg_pss_test), + CASE1(run_iceberg_vpss_test), + CASE1(run_iceberg_vpss_quorum_test), + CASE1(run_iceberg_keygen_test), + CASE1(run_iceberg_keygen_api_test), + CASE1(run_iceberg_dealer_kat_test), + CASE1(run_iceberg_foreign_index_test), + CASE1(run_iceberg_infinity_nonce_test), + CASE1(run_iceberg_bounds_test), + CASE1(run_iceberg_null_entry_test), + CASE1(run_iceberg_wire_test), + CASE1(run_iceberg_vectors_test), + CASE1(run_iceberg_keygen_cheat_test), + CASE1(run_iceberg_e2e_test), + CASE1(run_iceberg_c1_property_test), + CASE1(run_iceberg_parser_fuzz_test), + CASE1(run_iceberg_tweak_test), +}; + +#endif /* SECP256K1_MODULE_ICEBERG_TESTS_IMPL_H */ diff --git a/src/modules/iceberg/vectors.h b/src/modules/iceberg/vectors.h new file mode 100644 index 00000000..d15b7018 --- /dev/null +++ b/src/modules/iceberg/vectors.h @@ -0,0 +1,2446 @@ +/** + * Note: this file was autogenerated using test_vectors_iceberg_generate.py. + * Do not edit. To regenerate: + * + * git clone https://github.com/nkohen/Iceberg.git + * git -C Iceberg checkout 7b55ef6dc0dd6e11d1c14cb3bc0a675ff3487cce + * ./tools/test_vectors_iceberg_generate.py Iceberg > src/modules/iceberg/vectors.h + * + * There are no published Iceberg test vectors. These record what the reference + * does, so they catch the two implementations drifting apart, not a mistake + * they might both make. That makes this the one file here that cannot be rebuilt + * from what this repository contains, which is why the commit is named. + */ + +#include + +#define ICEBERG_VECTOR_MAX_PARTICIPANTS 9 +#define ICEBERG_VECTOR_MAX_SEEDS 126 + +struct iceberg_session_vector { + const char *label; + unsigned int n; + unsigned int t; + unsigned int mu; + size_t n_seeds; + unsigned char seeds[ICEBERG_VECTOR_MAX_SEEDS][32]; + unsigned char group_pk[33]; + unsigned char msg[32]; + unsigned char sid[32]; + unsigned char cosigner_pk[33]; + unsigned char cosigner_pubnonce[66]; + unsigned char group_aggnonce[66]; + unsigned char group_pubnonce[66]; + unsigned char group_psig[32]; + unsigned char pubshares[ICEBERG_VECTOR_MAX_PARTICIPANTS][33]; + unsigned char pubnonces[ICEBERG_VECTOR_MAX_PARTICIPANTS][66]; + unsigned char psigs[ICEBERG_VECTOR_MAX_PARTICIPANTS][32]; +}; + +static const struct iceberg_session_vector iceberg_session_vectors[] = { + { + "2of3", 3, 2, 3, 3, + { /* one seed per (t-1)-subset, in rank order */ + { + 0x9F, 0xA8, 0x99, 0xCA, 0x5B, 0xA3, 0xBE, + 0x21, 0x29, 0x70, 0x58, 0xEB, 0xD5, 0xB4, + 0xEC, 0x76, 0x48, 0x6E, 0x90, 0x25, 0x83, + 0x52, 0xC7, 0x94, 0xFE, 0x4C, 0x37, 0x89, + 0xC8, 0x9E, 0x2C, 0xEF + }, + { + 0x97, 0x81, 0xAD, 0x34, 0x80, 0xC7, 0x2B, + 0x2C, 0x90, 0x40, 0x35, 0xCA, 0xCF, 0xAD, + 0x3F, 0xDC, 0x71, 0xAF, 0x42, 0x4D, 0xC2, + 0xE7, 0x11, 0x2A, 0x8A, 0xEA, 0x91, 0x8C, + 0xBE, 0x4D, 0xED, 0x29 + }, + { + 0x10, 0xAE, 0x98, 0x22, 0x0C, 0xD5, 0xF5, + 0x7B, 0x57, 0xBF, 0x1F, 0xD9, 0x9A, 0xA2, + 0x0B, 0xDF, 0xB5, 0x11, 0x0F, 0x47, 0x2A, + 0x7B, 0x59, 0xEE, 0x60, 0xA3, 0xFF, 0x29, + 0x57, 0xE5, 0xB7, 0x59 + }, + }, + { /* group_pk */ + 0x02, 0x2E, 0x8C, 0x40, 0x09, 0x7D, 0x25, 0xA4, 0xEE, + 0x31, 0x75, 0xCF, 0x94, 0xEC, 0xED, 0x56, 0x79, 0xF3, + 0x20, 0x69, 0xBE, 0x2C, 0x6B, 0xE3, 0x00, 0x6F, 0x91, + 0x1A, 0xBD, 0x16, 0x05, 0x3B, 0x42 + }, + { /* msg */ + 0x87, 0xDF, 0x1F, 0xBE, 0xFA, 0x87, 0x42, 0x9A, 0xB7, + 0x2E, 0x71, 0xB0, 0x98, 0x6B, 0xB1, 0xC6, 0x28, 0x12, + 0x13, 0x7A, 0x5F, 0x4B, 0x65, 0xCC, 0x50, 0x82, 0x43, + 0x2D, 0x7C, 0xB8, 0xD5, 0x30 + }, + { /* sid */ + 0xFD, 0xC3, 0xA7, 0x6D, 0xA9, 0xFE, 0x39, 0x97, 0x21, + 0x9C, 0x86, 0xE1, 0x9A, 0xA2, 0x68, 0x55, 0x1E, 0xD8, + 0x3A, 0x9C, 0x97, 0x35, 0x67, 0x46, 0xDB, 0xF7, 0xA2, + 0x60, 0x7A, 0x9E, 0xFE, 0x60 + }, + { /* cosigner_pk */ + 0x03, 0x6D, 0x84, 0xFF, 0xA2, 0x3B, 0xFB, 0xE7, 0xB7, + 0xF3, 0x13, 0xCA, 0x33, 0xE9, 0x89, 0xB3, 0x30, 0x77, + 0x90, 0x95, 0x19, 0xCC, 0xE0, 0xE0, 0xDD, 0x26, 0x1F, + 0x7A, 0x72, 0xF1, 0x34, 0xAA, 0x73 + }, + { /* cosigner_pubnonce */ + 0x02, 0x08, 0xF9, 0xB7, 0x13, 0xE4, 0x94, 0x69, 0x4E, + 0x8D, 0x00, 0x62, 0x9E, 0x5C, 0xD1, 0xC6, 0x5F, 0x45, + 0xAD, 0x19, 0x75, 0x4D, 0xFC, 0x4D, 0x40, 0x72, 0x17, + 0x24, 0x58, 0x0F, 0xC0, 0xF8, 0x65, 0x03, 0x42, 0xB2, + 0x8D, 0x48, 0xAA, 0xC7, 0xD9, 0xB0, 0x93, 0x98, 0x63, + 0xB3, 0x5D, 0x7A, 0x39, 0x44, 0x6A, 0x9C, 0xA7, 0xDB, + 0x46, 0x9A, 0x86, 0xB1, 0xAC, 0xA4, 0xF9, 0x99, 0x97, + 0x05, 0x5D, 0xAA + }, + { /* group_aggnonce */ + 0x03, 0xF6, 0xD9, 0xA0, 0x14, 0xDC, 0x72, 0xEE, 0x98, + 0x45, 0xCD, 0xB2, 0x08, 0x5A, 0xD0, 0x05, 0x80, 0x7F, + 0x4D, 0xA4, 0xF6, 0xC2, 0xE2, 0x3D, 0x17, 0x75, 0xA8, + 0x9C, 0x4C, 0x2B, 0xA7, 0xED, 0xD2, 0x02, 0xA6, 0x7A, + 0xA9, 0x09, 0xE8, 0xCD, 0x2F, 0xB2, 0xA0, 0x4A, 0x28, + 0xE1, 0x15, 0x2A, 0xF8, 0x54, 0x31, 0xE6, 0xD9, 0x2E, + 0xD0, 0x41, 0x0B, 0xE7, 0xB1, 0xCE, 0x63, 0xC3, 0x2A, + 0x07, 0xD7, 0x79 + }, + { /* group_pubnonce */ + 0x03, 0xF6, 0xD9, 0xA0, 0x14, 0xDC, 0x72, 0xEE, 0x98, + 0x45, 0xCD, 0xB2, 0x08, 0x5A, 0xD0, 0x05, 0x80, 0x7F, + 0x4D, 0xA4, 0xF6, 0xC2, 0xE2, 0x3D, 0x17, 0x75, 0xA8, + 0x9C, 0x4C, 0x2B, 0xA7, 0xED, 0xD2, 0x03, 0xF2, 0x14, + 0x97, 0x2E, 0xB1, 0xB1, 0x4B, 0x36, 0x6E, 0x01, 0x0A, + 0x57, 0x28, 0x71, 0x2D, 0x77, 0x1B, 0x32, 0xCD, 0xB2, + 0x2C, 0x33, 0x0D, 0xD7, 0xC1, 0x55, 0x99, 0x63, 0x65, + 0x25, 0x0F, 0x43 + }, + { /* group_psig */ + 0x04, 0x67, 0x8E, 0x88, 0xA9, 0xD9, 0x2D, 0x71, 0xC8, + 0x58, 0x19, 0x49, 0xB3, 0x17, 0xDE, 0x43, 0x57, 0x5D, + 0x3E, 0xDF, 0x28, 0xF4, 0x61, 0x7F, 0x69, 0xBC, 0xD3, + 0x70, 0x4E, 0xC5, 0x31, 0x8D + }, + { /* pubshares, participant 1 first */ + { + 0x02, 0x62, 0x42, 0xC4, 0xDC, 0x40, 0x2D, + 0xFD, 0xD1, 0xAD, 0x86, 0xB7, 0x67, 0x03, + 0xAA, 0xFB, 0x63, 0x42, 0x4A, 0x95, 0x5F, + 0x46, 0x8D, 0x72, 0x3D, 0x25, 0x63, 0x80, + 0xBF, 0x15, 0x96, 0x9B, 0xCF + }, + { + 0x02, 0x89, 0xF4, 0x05, 0xFB, 0xF9, 0x6C, + 0xB6, 0xE1, 0x25, 0x6A, 0x1D, 0x3E, 0xA0, + 0x37, 0x83, 0x4D, 0xE9, 0x2E, 0xFF, 0xD3, + 0x9A, 0x45, 0x20, 0x04, 0xCB, 0xD6, 0x81, + 0x9C, 0xFD, 0x2D, 0x01, 0x40 + }, + { + 0x02, 0x0E, 0xD1, 0x97, 0xB4, 0x79, 0xC2, + 0x43, 0x6D, 0x7A, 0x4E, 0x92, 0x76, 0x0E, + 0x97, 0x85, 0x17, 0x6D, 0x1C, 0x8A, 0x49, + 0x8A, 0x21, 0x8A, 0x6D, 0xA9, 0x91, 0xD4, + 0xDB, 0x53, 0x1B, 0x03, 0x04 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x03, 0xFB, 0x5B, 0xC0, 0x01, 0x98, 0x4E, + 0x1E, 0x2B, 0x9F, 0xF2, 0xDD, 0x9B, 0x65, + 0x88, 0xBD, 0x66, 0x9F, 0x53, 0x52, 0xE3, + 0x7E, 0x36, 0xA6, 0xF8, 0xD8, 0x4E, 0x97, + 0xEF, 0x55, 0xE4, 0x6C, 0xC2, 0x03, 0xFC, + 0x4D, 0x99, 0x02, 0x88, 0x09, 0x67, 0xD5, + 0xBC, 0xD4, 0x41, 0x9D, 0x67, 0x1C, 0x9C, + 0x64, 0xC6, 0x1D, 0x60, 0xA1, 0xA2, 0xCE, + 0xE8, 0xEF, 0x67, 0x39, 0xE0, 0x85, 0x56, + 0xD5, 0x98, 0xEC + }, + { + 0x03, 0x5E, 0x2C, 0x92, 0x7D, 0x5B, 0xF4, + 0xF5, 0x0D, 0xC6, 0xA0, 0x3C, 0x99, 0xD4, + 0x11, 0x8A, 0xB2, 0xDF, 0x85, 0xB2, 0x8C, + 0x45, 0xE1, 0x57, 0xEB, 0x3C, 0x67, 0x53, + 0xD0, 0xF9, 0x58, 0x7F, 0x60, 0x02, 0x2C, + 0x70, 0x09, 0xB9, 0x13, 0x4D, 0x90, 0x6E, + 0xC3, 0xDA, 0x51, 0x95, 0x1F, 0x0B, 0x9E, + 0x81, 0x08, 0x68, 0x46, 0x05, 0x7C, 0x24, + 0x06, 0x35, 0x8A, 0x82, 0xA9, 0xDA, 0x9B, + 0x0B, 0x2A, 0x00 + }, + { + 0x02, 0x77, 0x35, 0x2C, 0x49, 0xA7, 0x65, + 0x46, 0xE6, 0x0D, 0x01, 0xD6, 0xE6, 0x34, + 0x23, 0x3A, 0x9A, 0x15, 0x8E, 0xD1, 0x18, + 0x3E, 0x89, 0x6B, 0x7D, 0xEB, 0xAC, 0x09, + 0xD9, 0xDE, 0x56, 0xBB, 0x64, 0x02, 0xE4, + 0x9E, 0x37, 0xDC, 0xEA, 0x4D, 0xD4, 0x80, + 0x20, 0x3A, 0xB8, 0xFD, 0xD5, 0x54, 0x9F, + 0x3E, 0x11, 0x8A, 0x1C, 0x68, 0xCD, 0xC5, + 0x76, 0xE8, 0x79, 0xA8, 0x81, 0xAC, 0x1C, + 0x46, 0xB7, 0x08 + }, + }, + { /* signature shares, the first t participants */ + { + 0xCD, 0x51, 0xC2, 0x9A, 0xDA, 0x7C, 0x1A, + 0x2D, 0xDA, 0xF3, 0xD5, 0x84, 0x5F, 0x80, + 0xFD, 0xAE, 0x03, 0x94, 0x1E, 0x8C, 0x33, + 0x8E, 0x68, 0x10, 0x50, 0x81, 0xE9, 0x7E, + 0x27, 0xBF, 0x7C, 0x3E + }, + { + 0x96, 0x3B, 0xF6, 0xAD, 0x0B, 0x1F, 0x06, + 0xE9, 0xED, 0x8F, 0x91, 0xBF, 0x0B, 0xEA, + 0x1D, 0x19, 0xF5, 0x1C, 0x21, 0x52, 0x8E, + 0xDF, 0xCE, 0x65, 0x77, 0x74, 0xA0, 0xFF, + 0x30, 0x83, 0x85, 0xAE + }, + }, + }, + { + "3of5", 5, 3, 5, 10, + { /* one seed per (t-1)-subset, in rank order */ + { + 0xC8, 0x4C, 0x03, 0x72, 0x7B, 0xD2, 0x1B, + 0x38, 0xD6, 0x0D, 0x8D, 0xB7, 0xC8, 0x36, + 0x15, 0x15, 0x21, 0xDC, 0x0D, 0x98, 0x93, + 0x8A, 0xB4, 0xA4, 0x78, 0x1F, 0xAE, 0xC1, + 0x25, 0xA8, 0x97, 0xF3 + }, + { + 0xCE, 0x91, 0x69, 0xDD, 0x9C, 0x35, 0x4D, + 0x62, 0x39, 0xA8, 0x8C, 0xC0, 0xC1, 0xEA, + 0x8A, 0x0B, 0x58, 0xB0, 0x77, 0x55, 0x4E, + 0x5C, 0x63, 0xB5, 0x19, 0xAE, 0x40, 0x31, + 0x81, 0x98, 0x8B, 0x2D + }, + { + 0x28, 0xAB, 0xC4, 0xC2, 0x24, 0x06, 0xCA, + 0x3B, 0x8F, 0xC9, 0x37, 0x25, 0x31, 0xC6, + 0x30, 0xF3, 0x78, 0x03, 0x7C, 0xDF, 0x6F, + 0x75, 0xA3, 0x1E, 0x44, 0x46, 0x34, 0xD2, + 0xC0, 0xA5, 0xEB, 0x8E + }, + { + 0xAC, 0x33, 0x26, 0x3A, 0x42, 0x77, 0x03, + 0x2E, 0x85, 0xB6, 0xEC, 0xD5, 0x1A, 0xCE, + 0xAA, 0x02, 0x31, 0xD8, 0x4E, 0x99, 0x4F, + 0x6A, 0xB6, 0x8C, 0x30, 0x82, 0x0A, 0x3B, + 0xB9, 0x2C, 0xA7, 0x48 + }, + { + 0xC6, 0x70, 0xA6, 0x6E, 0x79, 0xC5, 0x80, + 0x8F, 0x7A, 0xEB, 0xE6, 0xC1, 0x1D, 0x1C, + 0xB6, 0x61, 0x00, 0x5B, 0xB5, 0x75, 0x7F, + 0x73, 0x7C, 0xA9, 0x39, 0xDA, 0x8A, 0x68, + 0x84, 0x21, 0x92, 0xBD + }, + { + 0xA8, 0x69, 0x98, 0xDE, 0x0B, 0xD1, 0x7C, + 0x27, 0x34, 0x25, 0xCE, 0xF0, 0xBF, 0xFB, + 0x37, 0x94, 0xAC, 0xDB, 0x67, 0x2B, 0xD7, + 0xBA, 0x77, 0x26, 0x22, 0xF7, 0x32, 0x0C, + 0x37, 0x88, 0x0A, 0x4A + }, + { + 0x67, 0x58, 0x8C, 0xD3, 0xBA, 0x94, 0xF5, + 0x78, 0xBB, 0x04, 0x6D, 0xE0, 0x00, 0xDA, + 0x47, 0x23, 0xF5, 0xC1, 0xD0, 0xB0, 0xCE, + 0x70, 0xA3, 0x50, 0x32, 0x60, 0x36, 0x15, + 0x5A, 0x84, 0x83, 0x59 + }, + { + 0xD1, 0x4F, 0x88, 0xAE, 0x99, 0xDC, 0x70, + 0xBB, 0xC8, 0xC9, 0x3C, 0x5B, 0xAB, 0xF4, + 0x39, 0x6A, 0x59, 0xCF, 0xF0, 0x2F, 0x5A, + 0x60, 0x1B, 0x98, 0x42, 0x32, 0x4D, 0x02, + 0xD4, 0x1E, 0xF3, 0xF5 + }, + { + 0x8C, 0x3F, 0xE7, 0x2F, 0x98, 0x9F, 0x23, + 0xC5, 0x09, 0xC2, 0x72, 0x91, 0x5F, 0xD0, + 0x15, 0x84, 0x8A, 0x9E, 0x82, 0xA2, 0xB9, + 0xE2, 0xE4, 0x27, 0x56, 0x7F, 0x3E, 0x37, + 0x50, 0x19, 0xA5, 0x5E + }, + { + 0x42, 0xAE, 0x91, 0x42, 0x17, 0x2B, 0x39, + 0xE5, 0xEF, 0xBE, 0xEE, 0xCA, 0x0B, 0x1D, + 0xAF, 0x05, 0xA1, 0x17, 0x95, 0xC4, 0xDE, + 0xBF, 0x18, 0x19, 0x8D, 0x53, 0x92, 0x4C, + 0x10, 0xA3, 0xC6, 0x80 + }, + }, + { /* group_pk */ + 0x03, 0xF7, 0x5D, 0x9C, 0xE0, 0xFF, 0x0E, 0xBA, 0xC8, + 0x81, 0x4C, 0x2B, 0x9F, 0x15, 0xD4, 0x9C, 0x83, 0x50, + 0x3B, 0x5D, 0x26, 0xEB, 0x81, 0x9C, 0xC1, 0x90, 0xC0, + 0x44, 0x2B, 0x50, 0x39, 0xD2, 0x8B + }, + { /* msg */ + 0x3A, 0x08, 0xAE, 0x76, 0x9F, 0x7F, 0x64, 0x6B, 0xFC, + 0x54, 0x7E, 0x8F, 0x39, 0x77, 0x63, 0x2C, 0x64, 0x44, + 0x9A, 0xD7, 0x5E, 0x5D, 0x9C, 0xDD, 0xC4, 0xDC, 0x22, + 0x0A, 0x6F, 0xCC, 0x3C, 0x31 + }, + { /* sid */ + 0xF8, 0x94, 0xCE, 0x56, 0x9A, 0x98, 0xEC, 0x6A, 0x97, + 0x1B, 0x48, 0x30, 0x60, 0x56, 0x16, 0x13, 0xD1, 0x21, + 0x72, 0xC1, 0x2B, 0xE1, 0x09, 0xB5, 0xBF, 0xE7, 0xB3, + 0x26, 0xB8, 0x21, 0x1F, 0x5C + }, + { /* cosigner_pk */ + 0x02, 0x0C, 0x2B, 0xE9, 0x93, 0xC9, 0xED, 0x08, 0x86, + 0xFD, 0xF6, 0x92, 0xFA, 0x87, 0x3E, 0xC5, 0xB2, 0x88, + 0xFD, 0x75, 0x7C, 0x2F, 0x06, 0xE9, 0xBE, 0xA6, 0xE6, + 0xA1, 0xFC, 0x87, 0xC8, 0xE5, 0xB3 + }, + { /* cosigner_pubnonce */ + 0x03, 0x3A, 0x69, 0xDF, 0xCB, 0xB8, 0x35, 0x7C, 0xED, + 0xFD, 0x24, 0xBD, 0x59, 0xBC, 0x9C, 0xA8, 0x60, 0x7E, + 0x5C, 0xC3, 0x59, 0xF4, 0x42, 0xF0, 0xD2, 0xA5, 0xE4, + 0x85, 0xA0, 0x79, 0xFC, 0xD5, 0x12, 0x02, 0x59, 0x5C, + 0x62, 0xF1, 0x6E, 0x2E, 0xDD, 0x34, 0x96, 0x77, 0x54, + 0x01, 0xC7, 0xA9, 0x70, 0xCA, 0xD5, 0x04, 0x75, 0x64, + 0x9B, 0x81, 0x54, 0x92, 0x0B, 0xC8, 0x0D, 0x73, 0x6A, + 0x34, 0xF0, 0xC6 + }, + { /* group_aggnonce */ + 0x02, 0x4D, 0x52, 0x7D, 0xF9, 0x48, 0x79, 0x38, 0x2C, + 0xFB, 0x92, 0xE2, 0x8A, 0x18, 0x4C, 0x5A, 0x92, 0xB1, + 0xC7, 0x70, 0x41, 0xDC, 0x16, 0xDD, 0x18, 0x48, 0xFC, + 0x42, 0x53, 0x66, 0x63, 0x0B, 0x38, 0x02, 0xC8, 0xE6, + 0x34, 0x1E, 0x30, 0x96, 0xCB, 0xDF, 0x5C, 0xE3, 0xF6, + 0x4B, 0xE8, 0x44, 0x27, 0xE4, 0x38, 0xAD, 0xDB, 0xA7, + 0xD0, 0xDC, 0x29, 0xA7, 0xDC, 0x21, 0xF4, 0xAD, 0x42, + 0x65, 0x12, 0xFF + }, + { /* group_pubnonce */ + 0x02, 0x4D, 0x52, 0x7D, 0xF9, 0x48, 0x79, 0x38, 0x2C, + 0xFB, 0x92, 0xE2, 0x8A, 0x18, 0x4C, 0x5A, 0x92, 0xB1, + 0xC7, 0x70, 0x41, 0xDC, 0x16, 0xDD, 0x18, 0x48, 0xFC, + 0x42, 0x53, 0x66, 0x63, 0x0B, 0x38, 0x03, 0x56, 0x46, + 0xE9, 0xBD, 0xCF, 0xC0, 0xD9, 0xE6, 0x42, 0x9A, 0xD2, + 0x12, 0x48, 0x6C, 0x16, 0xBE, 0x56, 0x8D, 0x9C, 0xD7, + 0xC1, 0x0D, 0xB3, 0xED, 0x1D, 0xB0, 0x91, 0xAA, 0x23, + 0x1B, 0x20, 0x15 + }, + { /* group_psig */ + 0x43, 0xD5, 0xBB, 0x3F, 0x15, 0xFA, 0xAD, 0x42, 0x7D, + 0x3A, 0x5B, 0x66, 0xF2, 0xC9, 0x9A, 0xC4, 0xB8, 0xB2, + 0x25, 0x1A, 0x46, 0xFF, 0x15, 0x9E, 0x77, 0xF8, 0x37, + 0xA9, 0xDC, 0x33, 0xA9, 0x00 + }, + { /* pubshares, participant 1 first */ + { + 0x03, 0x7C, 0x8D, 0x04, 0x3A, 0x28, 0x53, + 0x1B, 0x44, 0xD9, 0xD7, 0xD6, 0xDC, 0xC6, + 0xF3, 0x43, 0x95, 0xC9, 0x1D, 0xA7, 0x1D, + 0x53, 0x9F, 0xD7, 0xE1, 0xE4, 0xEE, 0xB0, + 0x80, 0x2F, 0xF6, 0x35, 0x71 + }, + { + 0x02, 0x2E, 0xFA, 0x79, 0x67, 0xDA, 0xA1, + 0x67, 0xCB, 0x31, 0xDC, 0x4A, 0x33, 0x65, + 0x4C, 0x3C, 0x13, 0x6D, 0xD5, 0x71, 0x9B, + 0x83, 0xEC, 0xF6, 0x3D, 0x08, 0x11, 0x47, + 0xEC, 0x8C, 0xC1, 0xD0, 0x0A + }, + { + 0x03, 0x0E, 0xE0, 0x9A, 0xA0, 0xAB, 0x0A, + 0xCF, 0x04, 0x63, 0x99, 0xB5, 0xE7, 0xDA, + 0xD8, 0x2D, 0x03, 0xA1, 0x28, 0xB7, 0x31, + 0xBB, 0x15, 0x78, 0xF0, 0x43, 0x50, 0xED, + 0x84, 0x83, 0xDE, 0x4D, 0x61 + }, + { + 0x03, 0xF3, 0x99, 0xEF, 0x5B, 0x42, 0xF5, + 0xF9, 0x1B, 0xDB, 0x55, 0xBD, 0x28, 0xBD, + 0x2E, 0xCB, 0xC6, 0xC2, 0x4A, 0xEA, 0xB4, + 0x6C, 0x25, 0x02, 0x5F, 0xE1, 0x90, 0xDF, + 0xE7, 0x4C, 0x76, 0x9A, 0xF6 + }, + { + 0x02, 0x8A, 0xC9, 0x95, 0x39, 0x15, 0x0F, + 0xE7, 0x45, 0xAB, 0x05, 0x21, 0xAB, 0x99, + 0x6D, 0x6D, 0x72, 0xFD, 0x3D, 0xCA, 0x11, + 0x9B, 0x19, 0xFB, 0x57, 0xD3, 0xA0, 0xE4, + 0xCD, 0x4B, 0x28, 0x1B, 0xBD + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x02, 0xFF, 0xF3, 0x59, 0x35, 0xE0, 0x0B, + 0x3F, 0xAB, 0xD9, 0xF9, 0x98, 0x92, 0x5C, + 0x90, 0xEB, 0xCC, 0x9E, 0x4F, 0x7F, 0xA8, + 0x1C, 0xA3, 0x18, 0x3A, 0x5D, 0xAC, 0x45, + 0x24, 0x06, 0x3E, 0x76, 0x2A, 0x02, 0x70, + 0xE4, 0x98, 0x52, 0x7B, 0xF7, 0x1A, 0xCF, + 0x2C, 0x16, 0xC8, 0x5C, 0x82, 0x5D, 0x51, + 0xE2, 0xBA, 0x14, 0x4C, 0xA4, 0xAD, 0xCB, + 0xAC, 0xC1, 0x6B, 0xF8, 0x33, 0x47, 0x9C, + 0xCE, 0x75, 0xD1 + }, + { + 0x03, 0x20, 0x0E, 0x62, 0x8B, 0x01, 0x4E, + 0xD4, 0xEA, 0x5C, 0xBA, 0xA1, 0x9C, 0xDF, + 0x76, 0x40, 0x06, 0x89, 0x61, 0x50, 0x3E, + 0xC3, 0xEE, 0x5E, 0x4D, 0xE8, 0xFA, 0x8D, + 0x51, 0xEC, 0xD7, 0xB4, 0xAF, 0x03, 0xFA, + 0xD6, 0xDB, 0x0F, 0x05, 0x9A, 0x0D, 0xA1, + 0x43, 0x12, 0xC9, 0x4A, 0xD3, 0x9C, 0x9D, + 0xF9, 0xCD, 0x3C, 0x03, 0x50, 0x50, 0x0F, + 0xEC, 0xFC, 0x9E, 0x2F, 0x69, 0x6C, 0xC0, + 0xC3, 0xB3, 0x51 + }, + { + 0x03, 0x8A, 0x12, 0x1B, 0x78, 0xC7, 0x60, + 0x54, 0xC2, 0xFF, 0xE4, 0xF1, 0xB9, 0x1F, + 0xE8, 0x97, 0xE6, 0xE7, 0x78, 0x4A, 0xEB, + 0xBB, 0x11, 0x15, 0x92, 0xF5, 0x68, 0x80, + 0x7A, 0x05, 0x1C, 0x84, 0xF0, 0x02, 0x02, + 0x10, 0x9F, 0xCA, 0xCE, 0x30, 0x20, 0x3D, + 0x68, 0x98, 0x05, 0xDD, 0xE9, 0xCB, 0x66, + 0xE7, 0x9F, 0x53, 0x7F, 0x5E, 0xA5, 0x27, + 0x9D, 0x72, 0x99, 0xEB, 0x8D, 0xAB, 0xAB, + 0xF3, 0x48, 0x9A + }, + { + 0x03, 0x17, 0x02, 0xE2, 0x05, 0x1B, 0x35, + 0x40, 0x41, 0x46, 0x2C, 0x9C, 0x27, 0x9C, + 0x25, 0xC4, 0xEA, 0x90, 0x1E, 0xB3, 0x85, + 0x00, 0x9D, 0x9A, 0x6D, 0x34, 0x36, 0x72, + 0x3B, 0x4D, 0x65, 0x6E, 0xF1, 0x02, 0xA7, + 0xD9, 0xC7, 0x57, 0x9C, 0xEA, 0xB0, 0x80, + 0x6D, 0x6B, 0x18, 0x1D, 0x7D, 0x9E, 0x2D, + 0xF2, 0xCA, 0x67, 0x62, 0x8E, 0xAF, 0x15, + 0x7F, 0x7F, 0x80, 0x45, 0x62, 0x76, 0xB8, + 0xC5, 0x96, 0x51 + }, + { + 0x03, 0xEB, 0xBC, 0xB0, 0x88, 0x1C, 0xF2, + 0xD8, 0x36, 0x64, 0xE7, 0xBB, 0xA1, 0x36, + 0xC0, 0xA4, 0x22, 0x79, 0x8E, 0xEC, 0x9C, + 0x40, 0x84, 0xBC, 0x23, 0x98, 0xBD, 0xB4, + 0xE6, 0xEC, 0x03, 0xB6, 0x6D, 0x02, 0x06, + 0x5C, 0x57, 0xD0, 0x4C, 0x0A, 0xFF, 0x9C, + 0x51, 0xA3, 0xC4, 0x60, 0xFB, 0xAE, 0x36, + 0x00, 0x88, 0xB4, 0xB9, 0xE2, 0x14, 0x0F, + 0x24, 0xFF, 0xF7, 0x0D, 0x6B, 0x38, 0xA0, + 0x22, 0x5D, 0xAB + }, + }, + { /* signature shares, the first t participants */ + { + 0x29, 0x1D, 0x1E, 0xE1, 0x6A, 0xD7, 0xBB, + 0x42, 0x88, 0xB0, 0xB3, 0x8B, 0x5C, 0x62, + 0xEB, 0xD3, 0xCC, 0x27, 0xC8, 0xF8, 0x29, + 0xE2, 0x92, 0x2F, 0x7C, 0x47, 0xFC, 0x7F, + 0x1A, 0xB2, 0x5F, 0xE6 + }, + { + 0x91, 0xD7, 0x06, 0x68, 0xA6, 0xEF, 0x80, + 0xF8, 0x33, 0x7D, 0x28, 0x33, 0xDB, 0x1B, + 0xDA, 0x18, 0xD2, 0x91, 0xEC, 0x2C, 0x71, + 0x85, 0x08, 0x9A, 0x61, 0x3D, 0x58, 0x01, + 0x5F, 0x9C, 0x6B, 0xD7 + }, + { + 0x7E, 0x03, 0x71, 0xD4, 0xCA, 0x41, 0xFE, + 0x63, 0x7D, 0x9F, 0xB9, 0x60, 0x6E, 0xF4, + 0x65, 0x95, 0x11, 0x41, 0xB1, 0xD0, 0x6E, + 0x9D, 0xD8, 0xA3, 0x67, 0x05, 0xEB, 0xA3, + 0xDA, 0xBB, 0x8B, 0x92 + }, + }, + }, + { + "3of7", 7, 3, 5, 21, + { /* one seed per (t-1)-subset, in rank order */ + { + 0xE1, 0xA7, 0x4E, 0xA3, 0xE2, 0x9B, 0x48, + 0x39, 0x3C, 0x29, 0x28, 0x77, 0x44, 0x5B, + 0xBA, 0x31, 0xF1, 0x70, 0x9B, 0x9B, 0x2C, + 0xE5, 0x2B, 0x5D, 0xFA, 0x6F, 0x76, 0x62, + 0xF0, 0xE0, 0x66, 0x50 + }, + { + 0xEF, 0x62, 0x00, 0x9D, 0x07, 0x22, 0xE6, + 0x4F, 0xE8, 0x05, 0x39, 0x9B, 0x73, 0x82, + 0x66, 0xD3, 0x17, 0x9A, 0xB4, 0x48, 0x34, + 0xB2, 0xA9, 0x49, 0xCE, 0x26, 0xB6, 0x7D, + 0x9E, 0x60, 0xF6, 0xCF + }, + { + 0x18, 0x1B, 0xDD, 0x98, 0x1E, 0x06, 0x85, + 0xDA, 0x76, 0xC3, 0xC0, 0x50, 0xEA, 0x4C, + 0x71, 0x6A, 0xAF, 0xB4, 0x84, 0x9A, 0xF1, + 0x1D, 0xD3, 0xD1, 0x00, 0x02, 0xCE, 0x19, + 0xC4, 0x4D, 0xAF, 0x27 + }, + { + 0xB7, 0x48, 0x20, 0x00, 0x8A, 0x59, 0xCA, + 0x45, 0x2F, 0x0C, 0x2B, 0x1B, 0xC8, 0x9D, + 0xF8, 0x64, 0xBB, 0x40, 0x57, 0x40, 0xA2, + 0xA5, 0xCE, 0x6B, 0x83, 0x16, 0x22, 0xD7, + 0x86, 0xF0, 0xB4, 0x3F + }, + { + 0x0D, 0xEE, 0x4C, 0x98, 0x07, 0xF3, 0xE7, + 0xB4, 0x2F, 0x74, 0x21, 0xF8, 0xCB, 0xEE, + 0x9D, 0xEB, 0x7F, 0xD5, 0x2E, 0x31, 0xCE, + 0xAB, 0xB9, 0x3E, 0x5F, 0x52, 0xA9, 0xF3, + 0x7F, 0xE5, 0x76, 0xC9 + }, + { + 0x16, 0x67, 0x5D, 0xED, 0x27, 0x5B, 0xEE, + 0x94, 0x44, 0x3B, 0x57, 0xC3, 0x32, 0xA4, + 0x1C, 0x17, 0x40, 0x1A, 0x20, 0xD6, 0xDD, + 0xA8, 0x69, 0x25, 0xE5, 0x0E, 0x96, 0x0A, + 0x36, 0x93, 0xF4, 0xBF + }, + { + 0x58, 0x65, 0x59, 0x81, 0x2B, 0x8A, 0xDF, + 0x52, 0xE4, 0xFB, 0x89, 0x95, 0x69, 0x31, + 0xDC, 0x21, 0xE8, 0x8E, 0xD5, 0xD6, 0x48, + 0x3D, 0xA9, 0xB9, 0xB4, 0x1F, 0x6B, 0x70, + 0x8A, 0xFE, 0x7B, 0xE8 + }, + { + 0x92, 0x9A, 0x0F, 0x46, 0xA3, 0x0E, 0x9F, + 0xB7, 0x3E, 0xED, 0x3B, 0x26, 0xB9, 0x49, + 0x9F, 0x29, 0x95, 0x82, 0x70, 0x21, 0x71, + 0x3B, 0x31, 0x2F, 0x49, 0xB2, 0xFC, 0x86, + 0xAE, 0xB8, 0x18, 0x23 + }, + { + 0x71, 0x72, 0x00, 0xA8, 0x26, 0x77, 0xBC, + 0x97, 0xD5, 0x50, 0x79, 0x44, 0xB8, 0x1D, + 0x60, 0x2E, 0x05, 0xAA, 0x02, 0x47, 0xA1, + 0x81, 0xA5, 0xED, 0x7F, 0x96, 0x57, 0x15, + 0xA3, 0xAD, 0xD5, 0x5D + }, + { + 0xEE, 0x9A, 0x95, 0xB5, 0xF3, 0xAD, 0x2C, + 0x59, 0x72, 0x27, 0x17, 0x90, 0x81, 0x4E, + 0x65, 0x9A, 0xEF, 0x5E, 0x22, 0x2C, 0x2C, + 0x19, 0x66, 0x76, 0x13, 0x7D, 0x52, 0x86, + 0xF6, 0xC5, 0xFC, 0xB3 + }, + { + 0xD8, 0x9B, 0x46, 0x9C, 0x07, 0xE1, 0xBB, + 0xDC, 0x0C, 0x0F, 0x46, 0xE3, 0x4F, 0x73, + 0x94, 0xA8, 0xBA, 0x0B, 0xD2, 0xC7, 0x55, + 0xCA, 0x13, 0xED, 0x0A, 0x60, 0x3A, 0xA9, + 0xFA, 0xAC, 0x66, 0xF4 + }, + { + 0x90, 0x36, 0x57, 0xCE, 0x6D, 0x80, 0xB0, + 0x05, 0x17, 0xE6, 0x5F, 0xF4, 0x48, 0x1C, + 0xE4, 0x39, 0xA2, 0xD4, 0x6A, 0xFA, 0x1E, + 0xC6, 0xEB, 0xC8, 0x5A, 0x10, 0x6B, 0xB3, + 0x7B, 0x74, 0x8D, 0x44 + }, + { + 0xC8, 0x2F, 0x25, 0x58, 0xD6, 0x11, 0x23, + 0x15, 0x9E, 0xCA, 0xAD, 0xF1, 0xB1, 0x4E, + 0x55, 0x85, 0x8D, 0xC5, 0xBC, 0xDD, 0x3D, + 0x44, 0x61, 0xE9, 0x11, 0x5C, 0x5D, 0x30, + 0xFF, 0xE2, 0x46, 0xCE + }, + { + 0x80, 0xE0, 0xA4, 0xD0, 0xCB, 0x99, 0xE0, + 0xEB, 0x85, 0x0F, 0x67, 0xAE, 0x10, 0xB9, + 0x60, 0xAF, 0x64, 0x6E, 0x8A, 0xEC, 0xD9, + 0x8C, 0x10, 0x24, 0xB5, 0xF6, 0x46, 0x85, + 0x3F, 0x60, 0xA6, 0xEE + }, + { + 0x1E, 0xDB, 0xE5, 0x29, 0xCD, 0xAD, 0xFA, + 0x33, 0x96, 0x0A, 0x09, 0xF0, 0x8D, 0x0F, + 0x00, 0x61, 0xFB, 0x7E, 0xCD, 0x9C, 0xAD, + 0xE5, 0xF6, 0x4B, 0x29, 0x40, 0xA8, 0x00, + 0xE0, 0x07, 0xEB, 0xC1 + }, + { + 0x5D, 0x2C, 0xEC, 0x20, 0xDC, 0x6A, 0xE4, + 0x57, 0x5D, 0x4D, 0xCE, 0x0F, 0x77, 0x5C, + 0xBD, 0x0D, 0x2D, 0xF2, 0x05, 0x48, 0x93, + 0x33, 0x07, 0x52, 0xEB, 0xB7, 0x18, 0xC2, + 0x71, 0xDA, 0x4C, 0x6C + }, + { + 0xA7, 0xAE, 0xFB, 0xCE, 0x4A, 0xDA, 0x43, + 0x86, 0x39, 0x7F, 0x42, 0x79, 0xDF, 0xAC, + 0xD9, 0xEA, 0x9B, 0xFB, 0x7B, 0xE1, 0x06, + 0x3B, 0x20, 0xA7, 0x58, 0xF0, 0x46, 0x8F, + 0x11, 0xCD, 0xEF, 0x84 + }, + { + 0xC3, 0xFA, 0x8B, 0x8B, 0x0B, 0x1B, 0x7B, + 0xC5, 0xE0, 0xA7, 0xE4, 0x6F, 0x16, 0x1C, + 0xBD, 0x22, 0xF9, 0xA5, 0x95, 0x35, 0xF4, + 0xF1, 0xCF, 0xD8, 0xC0, 0x5D, 0xEB, 0x84, + 0xB1, 0xAA, 0x18, 0x2B + }, + { + 0x0A, 0xDA, 0x69, 0xFB, 0x82, 0x99, 0xBE, + 0x87, 0xB5, 0x9F, 0x8D, 0x7A, 0x55, 0x5D, + 0x88, 0xDF, 0x7E, 0x78, 0x5A, 0xD0, 0xC0, + 0x01, 0x27, 0x0E, 0x9F, 0xCB, 0xD0, 0x60, + 0x66, 0x50, 0x09, 0x71 + }, + { + 0x04, 0xF9, 0x27, 0x05, 0x18, 0xA0, 0x4F, + 0x7C, 0x5A, 0xF9, 0xEA, 0xD5, 0xC0, 0x75, + 0xC5, 0x05, 0xD7, 0xF0, 0x5D, 0x12, 0x72, + 0x3C, 0x79, 0x69, 0xCE, 0xF8, 0x90, 0x3F, + 0x71, 0x2D, 0xDE, 0xC0 + }, + { + 0xBB, 0x19, 0x98, 0xDD, 0xEA, 0xCB, 0xB2, + 0xF9, 0x00, 0x91, 0x84, 0x37, 0x81, 0x69, + 0x47, 0xFE, 0x07, 0xFD, 0x3C, 0xC1, 0x32, + 0x8E, 0x5E, 0xCF, 0x27, 0x2E, 0xA1, 0x4F, + 0xA6, 0x0E, 0xF4, 0xEA + }, + }, + { /* group_pk */ + 0x03, 0x20, 0x2C, 0x04, 0x57, 0xC7, 0xEB, 0x28, 0xEC, + 0x99, 0xAB, 0xEE, 0x04, 0xCF, 0x12, 0x55, 0x45, 0xC2, + 0x65, 0xF7, 0x5C, 0x26, 0x40, 0x58, 0x88, 0x99, 0x26, + 0x31, 0x61, 0xC2, 0x07, 0xD6, 0x25 + }, + { /* msg */ + 0xE6, 0xD2, 0x45, 0x9D, 0x0E, 0x22, 0xD9, 0x49, 0xFB, + 0xAF, 0x5D, 0x41, 0xC6, 0x4D, 0x22, 0x7D, 0x97, 0xAF, + 0x2F, 0x53, 0x52, 0x23, 0x7E, 0x5E, 0x07, 0x73, 0x1D, + 0x0A, 0x88, 0xA8, 0x68, 0x4A + }, + { /* sid */ + 0x73, 0x79, 0x75, 0x45, 0x65, 0x02, 0xC3, 0x34, 0x88, + 0x27, 0xFB, 0x8D, 0x2A, 0x75, 0x62, 0xA3, 0x37, 0x38, + 0x87, 0xD3, 0x24, 0x3B, 0xBC, 0x92, 0xFB, 0xF7, 0x12, + 0x71, 0x37, 0x4C, 0x63, 0x98 + }, + { /* cosigner_pk */ + 0x03, 0x92, 0x17, 0xED, 0x6B, 0x3D, 0x0A, 0xF6, 0x3E, + 0xB1, 0x2A, 0x80, 0x16, 0x87, 0x7B, 0x98, 0x2B, 0x5B, + 0xBF, 0x67, 0x16, 0xFE, 0x2B, 0x8A, 0x5C, 0x6D, 0x53, + 0x8B, 0xC1, 0x88, 0x7C, 0x20, 0x6D + }, + { /* cosigner_pubnonce */ + 0x02, 0x6B, 0x48, 0x73, 0x75, 0x85, 0x2A, 0x22, 0x98, + 0x79, 0x8C, 0x4F, 0x5A, 0xF8, 0x03, 0x4C, 0x20, 0xAC, + 0xD8, 0x46, 0x11, 0x82, 0x54, 0x10, 0xCF, 0xA2, 0x70, + 0xC5, 0xA3, 0x91, 0x08, 0x5E, 0x7C, 0x03, 0xB1, 0x84, + 0xEA, 0xB5, 0xC1, 0xE2, 0x30, 0xCB, 0x52, 0x87, 0x07, + 0x85, 0x26, 0xE9, 0xFE, 0xD1, 0xE9, 0xD3, 0x8A, 0x49, + 0x99, 0x24, 0x24, 0x92, 0x03, 0x12, 0x42, 0x8E, 0x1B, + 0xCD, 0x0E, 0xF9 + }, + { /* group_aggnonce */ + 0x02, 0x5A, 0xE2, 0xED, 0x71, 0x62, 0x4B, 0x63, 0x5D, + 0x95, 0x78, 0xA2, 0x24, 0x18, 0x0E, 0xBA, 0x7B, 0x29, + 0x8E, 0x3A, 0x13, 0xEA, 0xF3, 0xE2, 0xB1, 0x51, 0x96, + 0xE0, 0x69, 0x14, 0xF1, 0xD9, 0xA9, 0x03, 0x97, 0x01, + 0xD7, 0x7E, 0x78, 0xCD, 0x6E, 0x70, 0x75, 0x93, 0x12, + 0xBE, 0x82, 0xD5, 0x81, 0x93, 0x2F, 0x54, 0xDB, 0x13, + 0xF0, 0x58, 0xC5, 0xF9, 0xC4, 0x9A, 0x73, 0xBA, 0x99, + 0xF1, 0x23, 0x0A + }, + { /* group_pubnonce */ + 0x02, 0x5A, 0xE2, 0xED, 0x71, 0x62, 0x4B, 0x63, 0x5D, + 0x95, 0x78, 0xA2, 0x24, 0x18, 0x0E, 0xBA, 0x7B, 0x29, + 0x8E, 0x3A, 0x13, 0xEA, 0xF3, 0xE2, 0xB1, 0x51, 0x96, + 0xE0, 0x69, 0x14, 0xF1, 0xD9, 0xA9, 0x02, 0xFD, 0x20, + 0xD5, 0x5B, 0x84, 0x0C, 0x24, 0xF9, 0x79, 0x6E, 0xB6, + 0x39, 0xFE, 0xF4, 0x2A, 0x6F, 0x9C, 0xA0, 0x0C, 0x8C, + 0x49, 0xCE, 0x90, 0x10, 0x52, 0x85, 0xE6, 0xA5, 0x38, + 0x9B, 0xE1, 0xB3 + }, + { /* group_psig */ + 0xEB, 0xBC, 0x9F, 0x00, 0x93, 0xE2, 0xBF, 0x6C, 0x72, + 0xB3, 0x3D, 0xCD, 0xB7, 0x4E, 0x0C, 0x27, 0x20, 0x22, + 0xF2, 0x72, 0x79, 0xF2, 0xC6, 0xED, 0x19, 0xF9, 0xD3, + 0x5F, 0xFE, 0xF7, 0xC5, 0xDB + }, + { /* pubshares, participant 1 first */ + { + 0x03, 0x68, 0xC2, 0x21, 0x98, 0xC3, 0x52, + 0x62, 0xF8, 0x71, 0x80, 0x57, 0xAF, 0x52, + 0x12, 0xC0, 0x14, 0xCB, 0x23, 0x09, 0x0C, + 0x7F, 0x9C, 0x08, 0xAB, 0xF8, 0xF4, 0x9F, + 0x36, 0xD3, 0x14, 0x4D, 0x53 + }, + { + 0x02, 0xBD, 0x1B, 0x71, 0xCE, 0xDD, 0x27, + 0x82, 0xD6, 0xD3, 0x37, 0xDA, 0x23, 0xDA, + 0x06, 0x0C, 0x92, 0xD5, 0x4E, 0x9A, 0x53, + 0x05, 0x50, 0x5A, 0x87, 0x7E, 0x06, 0x99, + 0x98, 0x65, 0x06, 0xA5, 0x42 + }, + { + 0x02, 0x35, 0xE7, 0x4D, 0xF6, 0x3C, 0x97, + 0x0E, 0xA8, 0xA3, 0x26, 0x17, 0x47, 0x1B, + 0xB6, 0xA5, 0x94, 0xCB, 0x9A, 0xBB, 0x2E, + 0x63, 0x84, 0x70, 0x8D, 0xE1, 0x93, 0xD7, + 0xD8, 0x3D, 0x04, 0xF5, 0x7D + }, + { + 0x02, 0x99, 0xAF, 0x46, 0x8F, 0x7D, 0x85, + 0x7E, 0x9A, 0x19, 0x47, 0xA6, 0x46, 0xBC, + 0xF6, 0x6B, 0x84, 0xE1, 0x25, 0x24, 0x98, + 0xA1, 0x9C, 0xCF, 0xA4, 0x5B, 0x75, 0xDD, + 0x87, 0x5E, 0xE3, 0x54, 0xB1 + }, + { + 0x03, 0x3E, 0xAD, 0x34, 0x7E, 0x1F, 0x9F, + 0x1C, 0x90, 0xCC, 0xA6, 0xA7, 0x9E, 0x94, + 0x70, 0xC1, 0x46, 0xC5, 0x4F, 0x43, 0x35, + 0xD9, 0xE0, 0x99, 0xF5, 0x4B, 0x25, 0xE5, + 0xF5, 0x97, 0xDD, 0x56, 0x8E + }, + { + 0x02, 0x11, 0xF9, 0x43, 0x86, 0x20, 0x6C, + 0x0A, 0x0A, 0xC8, 0x3A, 0xCC, 0x44, 0xB9, + 0x2C, 0x32, 0x59, 0xB2, 0x33, 0xE7, 0xC5, + 0x26, 0x02, 0x82, 0x6B, 0x27, 0xB0, 0xB3, + 0xDC, 0xF3, 0x9E, 0x0D, 0x69 + }, + { + 0x02, 0x71, 0x37, 0x40, 0x46, 0x3C, 0xDF, + 0x2F, 0xAC, 0x9E, 0x70, 0xB8, 0x90, 0xD4, + 0x30, 0xA8, 0x55, 0x44, 0xBA, 0xC7, 0x01, + 0xE5, 0xFE, 0x86, 0x5B, 0x18, 0x46, 0xF2, + 0xBA, 0xC2, 0xDF, 0x68, 0xE3 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x03, 0xAD, 0x81, 0x3A, 0xA8, 0x13, 0xFC, + 0x44, 0x39, 0xB0, 0x5B, 0xC6, 0x31, 0xA4, + 0xC8, 0x2D, 0x62, 0x57, 0xDD, 0x76, 0xDE, + 0x95, 0x22, 0xF1, 0x32, 0x99, 0x4D, 0xA5, + 0x7B, 0xA9, 0xCC, 0x41, 0x2C, 0x02, 0x8A, + 0xA1, 0x12, 0xC1, 0x38, 0xA7, 0x95, 0xCE, + 0x25, 0x4B, 0xE3, 0xBE, 0x0E, 0x20, 0x36, + 0xE3, 0x32, 0x35, 0x4D, 0xBF, 0x07, 0x26, + 0x8A, 0x35, 0xA0, 0xB8, 0x97, 0xEB, 0x67, + 0x83, 0xC1, 0xC0 + }, + { + 0x03, 0xEB, 0xC3, 0x0F, 0x61, 0x7F, 0x9D, + 0x52, 0x5B, 0xE7, 0xBE, 0xB0, 0xB8, 0x13, + 0x83, 0xE5, 0xE3, 0xB6, 0x72, 0x25, 0x6C, + 0x9B, 0x3D, 0x32, 0x8A, 0x88, 0xB8, 0x40, + 0x1F, 0x2D, 0xDB, 0x00, 0x84, 0x02, 0x4E, + 0x15, 0x0D, 0xD3, 0x0E, 0xE7, 0xBA, 0x33, + 0x7A, 0xB5, 0x57, 0x15, 0x90, 0x58, 0xB6, + 0xC1, 0xB0, 0xED, 0xCD, 0xC1, 0x32, 0x2B, + 0xE7, 0x92, 0x7C, 0xEC, 0xF5, 0x3A, 0x64, + 0xD9, 0x89, 0xCB + }, + { + 0x03, 0x32, 0x57, 0x98, 0xA2, 0x58, 0xC3, + 0x00, 0xB8, 0x39, 0x2A, 0x98, 0x60, 0xFF, + 0xED, 0xB6, 0x4F, 0x23, 0x6F, 0x86, 0x1D, + 0x8C, 0x22, 0xA1, 0x4B, 0x65, 0x9D, 0x4E, + 0xBD, 0xA7, 0xC4, 0xE5, 0x2C, 0x03, 0x48, + 0x1C, 0xC6, 0x39, 0x9A, 0x33, 0x1D, 0xCD, + 0x8A, 0xB7, 0x17, 0x87, 0x05, 0x38, 0xEA, + 0x76, 0x0D, 0xFE, 0x16, 0x8C, 0x08, 0xD5, + 0x3F, 0x7F, 0x9C, 0x96, 0x2C, 0xEA, 0xE0, + 0x56, 0xA5, 0xF0 + }, + { + 0x02, 0xA2, 0x9A, 0x9F, 0x85, 0x38, 0x8C, + 0x9B, 0xDF, 0xA1, 0x2D, 0xF4, 0xE3, 0x46, + 0xB3, 0xD4, 0xE7, 0xFF, 0x6B, 0x3F, 0xD4, + 0x79, 0xB0, 0x34, 0x53, 0x0E, 0x20, 0x0A, + 0x64, 0x64, 0x9D, 0x30, 0x2F, 0x02, 0x32, + 0xB7, 0x32, 0x18, 0x3B, 0x62, 0xEE, 0x6C, + 0xBF, 0x40, 0xBD, 0x32, 0xA0, 0x3F, 0x70, + 0x93, 0x59, 0x0A, 0xA5, 0xFD, 0x75, 0xF1, + 0xFF, 0x55, 0xF8, 0xE8, 0x0F, 0x11, 0x9D, + 0x56, 0xC0, 0x1C + }, + { + 0x03, 0x18, 0x37, 0xE3, 0xE0, 0x47, 0x1A, + 0x93, 0xBA, 0x65, 0xAC, 0xB7, 0x43, 0x0E, + 0xCD, 0x29, 0xD5, 0x70, 0xEC, 0x1D, 0xFE, + 0x5A, 0x4D, 0xAB, 0x32, 0xE7, 0x0F, 0xE2, + 0x0F, 0x1C, 0x28, 0x1A, 0xAC, 0x03, 0x5E, + 0xA1, 0x51, 0x8E, 0x68, 0x43, 0xAF, 0xAE, + 0x01, 0xF3, 0xCC, 0x76, 0x99, 0xAB, 0x87, + 0x06, 0xDE, 0xB5, 0xC8, 0x0A, 0x20, 0xE3, + 0x4A, 0xB5, 0x89, 0x9C, 0xF9, 0xA1, 0x26, + 0xA2, 0xC6, 0x35 + }, + }, + { /* signature shares, the first t participants */ + { + 0x16, 0x50, 0xF8, 0xA4, 0x22, 0x8A, 0x01, + 0x55, 0xBB, 0x1D, 0xB8, 0x61, 0xC9, 0x5B, + 0xB4, 0xFF, 0xC1, 0xBD, 0x53, 0x39, 0xF2, + 0xC1, 0x01, 0x0C, 0x62, 0x8E, 0x9E, 0x8F, + 0x6A, 0xAF, 0x0C, 0xE3 + }, + { + 0x91, 0x47, 0x44, 0xF6, 0xAD, 0x49, 0x79, + 0xC7, 0x1A, 0x8D, 0x1F, 0xA9, 0xBC, 0xAA, + 0xBE, 0xAC, 0xD4, 0xD1, 0x70, 0xB2, 0x00, + 0x2E, 0xEE, 0x14, 0xDF, 0x1E, 0x12, 0x66, + 0xDD, 0x25, 0x55, 0x64 + }, + { + 0x5C, 0x9F, 0x83, 0xF8, 0x34, 0x21, 0x28, + 0xC0, 0x91, 0x01, 0x73, 0xA5, 0x91, 0x3B, + 0x29, 0x30, 0xE4, 0x01, 0x91, 0x0D, 0x43, + 0xAB, 0x4D, 0x8F, 0x10, 0x03, 0x71, 0xCC, + 0xB5, 0xEE, 0x1C, 0xDC + }, + }, + }, + { + "4of7", 7, 4, 7, 35, + { /* one seed per (t-1)-subset, in rank order */ + { + 0xE9, 0x87, 0x56, 0x9D, 0xA4, 0x2D, 0xC0, + 0x91, 0x6E, 0xBC, 0x56, 0x65, 0x94, 0x2F, + 0x96, 0x9B, 0xCD, 0xF5, 0xAD, 0x02, 0xB2, + 0x29, 0xAE, 0x31, 0x52, 0x2C, 0xF3, 0x1E, + 0x0B, 0x26, 0x87, 0x2E + }, + { + 0xBB, 0x75, 0x2D, 0xD0, 0x64, 0x6A, 0x11, + 0x0F, 0xA5, 0x8D, 0x7B, 0xFA, 0x59, 0xF0, + 0xF8, 0xCB, 0xD0, 0x9D, 0x8B, 0x86, 0x47, + 0x1C, 0x5E, 0x12, 0x2D, 0xFD, 0xD4, 0x6E, + 0xF8, 0xD6, 0x89, 0x8C + }, + { + 0xF8, 0xB2, 0xD5, 0xA8, 0xE2, 0xD5, 0xD7, + 0x67, 0x43, 0xA6, 0x1A, 0x68, 0xF8, 0x02, + 0x6F, 0xB2, 0xC0, 0x19, 0xFB, 0xDC, 0xAC, + 0x8F, 0x71, 0x9F, 0xE8, 0x24, 0xFE, 0x97, + 0xA9, 0x31, 0xC6, 0x39 + }, + { + 0x06, 0x50, 0xF6, 0xD2, 0x7E, 0x3D, 0x22, + 0x7C, 0xBD, 0xAD, 0x81, 0xB2, 0x94, 0xDB, + 0x5F, 0xA0, 0x9B, 0xC5, 0x81, 0x15, 0xCB, + 0x90, 0xCF, 0xBA, 0x9A, 0x2D, 0x63, 0x0A, + 0xD8, 0x11, 0xD9, 0xF2 + }, + { + 0x6A, 0x1E, 0x3A, 0xF4, 0x69, 0xB1, 0x99, + 0xCC, 0xF9, 0x21, 0x37, 0x16, 0x0E, 0x3B, + 0xCB, 0x9D, 0x65, 0x04, 0x46, 0xC2, 0x33, + 0x23, 0x87, 0xBF, 0x9C, 0x63, 0x41, 0x6C, + 0x9D, 0x65, 0xE8, 0xB2 + }, + { + 0xE9, 0x00, 0x04, 0x2E, 0x1A, 0xC9, 0x82, + 0xD6, 0xC6, 0xA7, 0x38, 0x7B, 0xAB, 0xBA, + 0xD6, 0x16, 0xDB, 0x0F, 0x62, 0x70, 0xFE, + 0x6A, 0xF6, 0x26, 0x0D, 0x06, 0x7E, 0x69, + 0x6E, 0x72, 0x61, 0x1B + }, + { + 0xB0, 0xCF, 0x62, 0x36, 0xA4, 0x28, 0x0A, + 0x85, 0xEB, 0x57, 0x2B, 0xE7, 0x59, 0xFF, + 0xDA, 0x80, 0xBE, 0x11, 0x99, 0x94, 0xEB, + 0x89, 0x3A, 0x66, 0x82, 0x17, 0xC9, 0xF9, + 0x52, 0x7B, 0xE9, 0x73 + }, + { + 0x73, 0x14, 0x6D, 0x2B, 0x7C, 0x38, 0x07, + 0x04, 0xB5, 0xD1, 0x33, 0x02, 0x87, 0xF7, + 0x69, 0x29, 0xEB, 0xBC, 0xBD, 0x67, 0x16, + 0x1D, 0x8F, 0x2C, 0x92, 0x86, 0x6E, 0xB4, + 0xE2, 0xC5, 0xAC, 0x4A + }, + { + 0x96, 0xF8, 0x89, 0xB2, 0x71, 0x65, 0x24, + 0x96, 0x14, 0x64, 0x64, 0xFD, 0xF5, 0x87, + 0x0C, 0x09, 0xDF, 0xD2, 0x95, 0xD0, 0x54, + 0x70, 0xCF, 0xB9, 0xA7, 0xED, 0xFA, 0xC1, + 0x80, 0x70, 0x97, 0xAF + }, + { + 0x91, 0xC1, 0xC1, 0x0B, 0x06, 0x5A, 0x10, + 0x78, 0xB9, 0x30, 0x78, 0xB9, 0x0B, 0x90, + 0xB9, 0x4F, 0x48, 0xBB, 0xEE, 0x39, 0xE1, + 0x05, 0xE5, 0x17, 0x2C, 0xA4, 0x94, 0x4D, + 0x3A, 0x72, 0xAD, 0xAD + }, + { + 0x99, 0x2E, 0x01, 0xDF, 0x39, 0x2A, 0xCE, + 0xA5, 0xFF, 0x9A, 0x74, 0x89, 0xB5, 0x64, + 0x39, 0xFC, 0xF7, 0xD8, 0xD6, 0xD6, 0xC9, + 0x85, 0x74, 0xA4, 0xA5, 0x0D, 0xAA, 0xA7, + 0xD1, 0x83, 0x7E, 0xB1 + }, + { + 0x5F, 0xF6, 0xF6, 0xEA, 0x60, 0x70, 0x98, + 0xA1, 0x73, 0xEA, 0xBF, 0xC5, 0x48, 0xCE, + 0x26, 0x09, 0x03, 0x69, 0xCF, 0x80, 0xA2, + 0xFE, 0x29, 0xA1, 0x0A, 0x51, 0x20, 0x39, + 0xC3, 0xBD, 0x20, 0x3E + }, + { + 0x92, 0x2B, 0x2B, 0xD9, 0x90, 0x5B, 0x3B, + 0x44, 0x69, 0xAD, 0x5C, 0xD9, 0x6F, 0x89, + 0x9E, 0x92, 0xC3, 0xA8, 0xB9, 0x9D, 0xCE, + 0x3F, 0xEA, 0x11, 0x63, 0xE1, 0x2C, 0xC7, + 0x55, 0xC1, 0x8B, 0xCE + }, + { + 0x4A, 0xC9, 0xA2, 0x2E, 0x8E, 0x69, 0x3F, + 0xB8, 0x3D, 0x34, 0x7C, 0x22, 0x1B, 0x7E, + 0x79, 0x51, 0x50, 0xD4, 0xF5, 0x34, 0x47, + 0x74, 0x02, 0x09, 0x3F, 0x01, 0x3A, 0xC9, + 0x4B, 0xAC, 0xE7, 0xB6 + }, + { + 0x5F, 0x8B, 0x9A, 0xD4, 0x69, 0xCF, 0xF7, + 0xE2, 0xA5, 0x43, 0x22, 0xAE, 0x33, 0xA5, + 0xFE, 0x8A, 0x16, 0x67, 0xA8, 0x85, 0xC1, + 0xAC, 0x6C, 0x29, 0x31, 0xB1, 0x10, 0x6D, + 0x89, 0x68, 0x50, 0x21 + }, + { + 0xC3, 0xF5, 0xB5, 0x09, 0x59, 0x9B, 0x79, + 0x64, 0x09, 0x03, 0x69, 0x35, 0x38, 0x01, + 0xCB, 0xB1, 0xCD, 0x31, 0x31, 0x0A, 0x61, + 0x0E, 0x6E, 0x5B, 0x83, 0x2D, 0xFB, 0x78, + 0xF7, 0xCA, 0x1A, 0x8D + }, + { + 0x4C, 0xFE, 0xE9, 0x2A, 0x8B, 0x74, 0xE2, + 0xA8, 0x73, 0x98, 0x68, 0xE9, 0xFE, 0x41, + 0x65, 0xC9, 0xA1, 0xED, 0x4D, 0x9F, 0xFC, + 0xBC, 0xE9, 0xA5, 0x4C, 0x55, 0x2D, 0x9C, + 0x55, 0x3C, 0xBD, 0xDE + }, + { + 0x2A, 0xD5, 0x0C, 0x10, 0x0A, 0xAE, 0x0D, + 0xE3, 0x7E, 0xC9, 0xF6, 0x57, 0x9A, 0xB3, + 0xAD, 0x28, 0x2E, 0x39, 0x6D, 0xC6, 0x1A, + 0xDC, 0x9E, 0xFA, 0x02, 0xD7, 0x06, 0x24, + 0x08, 0xF6, 0x7A, 0x63 + }, + { + 0x41, 0xD9, 0x79, 0x9E, 0x0F, 0x3C, 0x98, + 0xFD, 0x5B, 0xE8, 0xEA, 0x52, 0x3B, 0x2E, + 0x05, 0x35, 0x7A, 0xB6, 0x12, 0x67, 0x61, + 0x33, 0x6D, 0x9C, 0x10, 0xA5, 0x46, 0x56, + 0x42, 0xF7, 0x12, 0x5A + }, + { + 0x3B, 0xC9, 0x8F, 0xF6, 0x11, 0xF8, 0xA7, + 0x2D, 0xC6, 0xCB, 0x15, 0xCF, 0x68, 0xD8, + 0xBB, 0xA4, 0xCC, 0xCE, 0xDC, 0x1D, 0x50, + 0x70, 0x82, 0x37, 0x85, 0x51, 0xEE, 0xA4, + 0xCA, 0x9D, 0x26, 0x42 + }, + { + 0x4A, 0x49, 0xD2, 0x89, 0xF1, 0x51, 0x8D, + 0x82, 0xF8, 0x64, 0x33, 0xD9, 0x11, 0x08, + 0xDE, 0x5E, 0x53, 0xAB, 0x1C, 0xAF, 0x5D, + 0x71, 0x97, 0x87, 0xC9, 0xB9, 0x36, 0xCD, + 0x6B, 0x26, 0x2B, 0xE0 + }, + { + 0x0D, 0xC9, 0xCD, 0x73, 0xDD, 0xF5, 0x8B, + 0x40, 0xAF, 0xB6, 0x70, 0x0B, 0x56, 0x78, + 0x69, 0x18, 0x5A, 0x02, 0xA8, 0x7D, 0x2B, + 0x88, 0x83, 0x45, 0x91, 0x65, 0x7D, 0x71, + 0xA2, 0x25, 0x76, 0x6F + }, + { + 0xC6, 0x4D, 0x54, 0x72, 0x00, 0x43, 0xC5, + 0xDF, 0x01, 0x7D, 0xBC, 0xA8, 0xDB, 0xAA, + 0xBE, 0x1A, 0xB3, 0x05, 0x65, 0xEF, 0x2A, + 0xDA, 0xEA, 0xEC, 0x2E, 0xAB, 0xB9, 0x97, + 0xDC, 0x48, 0x2C, 0x61 + }, + { + 0xE5, 0x09, 0x06, 0x0D, 0x75, 0x7C, 0xCF, + 0x38, 0x10, 0xF7, 0xDE, 0x5A, 0x13, 0x1C, + 0xE8, 0x95, 0x6E, 0xA0, 0x45, 0x5E, 0x60, + 0x4F, 0x94, 0x73, 0x49, 0x18, 0x26, 0xBC, + 0xF3, 0x45, 0x2C, 0xE0 + }, + { + 0xBD, 0xA7, 0xFE, 0x41, 0x7A, 0x4F, 0x2B, + 0x1B, 0xD5, 0x50, 0x88, 0xA4, 0xB7, 0x19, + 0x11, 0xFC, 0x1F, 0x9B, 0x67, 0xD3, 0x00, + 0x0F, 0x27, 0xB5, 0xC7, 0xA3, 0xD2, 0x77, + 0x29, 0xA4, 0x65, 0x1C + }, + { + 0x29, 0x64, 0xFD, 0xFC, 0xCB, 0x6B, 0x6D, + 0xB7, 0xD4, 0x3A, 0x5F, 0x29, 0x7A, 0x44, + 0x5C, 0x3D, 0x08, 0x83, 0x8A, 0x63, 0xA3, + 0x88, 0x4B, 0x07, 0x56, 0xFC, 0x29, 0x1B, + 0x8E, 0x98, 0xD6, 0x99 + }, + { + 0x3F, 0xFB, 0x31, 0x66, 0x44, 0xB2, 0x86, + 0x15, 0x3C, 0x39, 0xBF, 0x9E, 0x90, 0xA4, + 0x71, 0x03, 0xF8, 0x7D, 0x5D, 0x54, 0x3A, + 0x42, 0x8D, 0x5C, 0x50, 0xBA, 0x51, 0x2F, + 0x15, 0xB7, 0x3A, 0x4D + }, + { + 0x4B, 0x12, 0x83, 0x94, 0x39, 0xAB, 0xA7, + 0x22, 0xC3, 0x92, 0xA4, 0x5C, 0x38, 0xF2, + 0x0C, 0x2A, 0xBF, 0xC2, 0x11, 0xF2, 0x2B, + 0x5F, 0xCC, 0x6D, 0x88, 0xDF, 0x17, 0x8F, + 0xBD, 0x19, 0x7A, 0x52 + }, + { + 0xC5, 0x65, 0x84, 0xE7, 0x0C, 0xF3, 0x3F, + 0x57, 0xA4, 0xF9, 0x07, 0x03, 0xC1, 0x02, + 0x86, 0x0A, 0x31, 0xAC, 0xBF, 0xA3, 0x15, + 0x7A, 0x1D, 0xF4, 0x34, 0x0F, 0xAE, 0x35, + 0x8D, 0xBF, 0x2E, 0x72 + }, + { + 0xCE, 0x52, 0x0D, 0x74, 0x54, 0xD1, 0x60, + 0x9C, 0x36, 0xAA, 0x79, 0x3A, 0x61, 0xCB, + 0xE0, 0x30, 0xAA, 0xD9, 0xBE, 0x80, 0x75, + 0x27, 0xFC, 0x35, 0x46, 0xAA, 0xAF, 0xFE, + 0xFC, 0x0E, 0x8C, 0x93 + }, + { + 0xB3, 0x2D, 0xBA, 0xDF, 0x25, 0x0F, 0x84, + 0x27, 0xEB, 0xBE, 0xAB, 0x82, 0x60, 0x92, + 0x9E, 0x26, 0x2A, 0xE1, 0xE4, 0xB9, 0x07, + 0x6D, 0x06, 0xB3, 0x6C, 0x61, 0x18, 0xB8, + 0x01, 0x95, 0x64, 0xB0 + }, + { + 0xEA, 0xD5, 0x8E, 0xE0, 0x0B, 0xB0, 0x00, + 0x78, 0x68, 0x6F, 0x55, 0x3F, 0xED, 0xD1, + 0x17, 0x32, 0x80, 0x78, 0x07, 0xC2, 0xC2, + 0x66, 0xC1, 0x96, 0xC8, 0xB0, 0xEE, 0x2C, + 0x2F, 0x9E, 0x6D, 0xB4 + }, + { + 0xBB, 0xA6, 0x29, 0xA1, 0x56, 0x59, 0x5F, + 0xC3, 0x6C, 0x5D, 0xC6, 0xD7, 0xC1, 0xD7, + 0x82, 0x41, 0x28, 0xD1, 0xDF, 0xFA, 0x72, + 0x32, 0xF0, 0xFB, 0xFC, 0x7B, 0x88, 0x21, + 0xE4, 0xD0, 0x50, 0x5A + }, + { + 0xB6, 0x79, 0x6C, 0x04, 0xEB, 0x61, 0xC4, + 0xF2, 0xB3, 0xA7, 0xEE, 0x42, 0x2B, 0x56, + 0xC7, 0x26, 0x10, 0x01, 0x94, 0xC4, 0x51, + 0xF2, 0xDD, 0x4E, 0x85, 0xBF, 0x2B, 0x34, + 0xA8, 0xE5, 0x7E, 0xA2 + }, + { + 0x49, 0xAC, 0xF7, 0xC8, 0xFE, 0xE6, 0x90, + 0xD0, 0xCB, 0x0F, 0x86, 0x29, 0x1F, 0x7A, + 0xDC, 0xA4, 0x58, 0x40, 0x36, 0x21, 0x05, + 0x60, 0xD0, 0xBE, 0x70, 0x6B, 0x19, 0x84, + 0xD9, 0xDD, 0xD2, 0x81 + }, + }, + { /* group_pk */ + 0x02, 0xEF, 0x23, 0x01, 0xB1, 0x3A, 0xCE, 0x73, 0x05, + 0x31, 0x76, 0xD2, 0x66, 0x14, 0xA8, 0x4C, 0xD5, 0x14, + 0xF2, 0xBB, 0x5E, 0x70, 0x98, 0xDD, 0xEB, 0xF3, 0x5D, + 0xAE, 0x5F, 0x99, 0x1B, 0xB5, 0x91 + }, + { /* msg */ + 0x8B, 0x4C, 0x7E, 0xC2, 0xA0, 0x7F, 0x68, 0x74, 0xB5, + 0xDB, 0xE7, 0x82, 0x38, 0xED, 0xAB, 0x02, 0x72, 0xAB, + 0x01, 0x61, 0xC6, 0x2B, 0xF5, 0xF6, 0x4A, 0xE7, 0xEA, + 0x2C, 0xBD, 0x77, 0x33, 0x21 + }, + { /* sid */ + 0x5B, 0xB9, 0xD8, 0xF8, 0x87, 0xFD, 0x3D, 0x52, 0xB6, + 0xBA, 0x1B, 0x5F, 0x78, 0x03, 0x3E, 0xEA, 0xE8, 0x29, + 0x1D, 0xF9, 0xBE, 0x21, 0x96, 0x95, 0x34, 0x24, 0x55, + 0xAE, 0xD6, 0x81, 0x2A, 0x82 + }, + { /* cosigner_pk */ + 0x02, 0xEA, 0x72, 0xD7, 0x97, 0x42, 0xF5, 0x35, 0x5F, + 0xF8, 0x3C, 0x4C, 0x96, 0xDD, 0xC1, 0xE7, 0xBD, 0x35, + 0x47, 0x4D, 0x85, 0x97, 0x62, 0x12, 0xE7, 0xFE, 0xA6, + 0x32, 0x63, 0x36, 0xE3, 0x4E, 0x93 + }, + { /* cosigner_pubnonce */ + 0x03, 0xA5, 0xC3, 0xD1, 0x66, 0x24, 0x48, 0xD9, 0xCC, + 0xCA, 0x44, 0x5F, 0xAE, 0x11, 0xD0, 0xD1, 0x14, 0x45, + 0xAA, 0x69, 0x32, 0xAF, 0x75, 0xA0, 0xE0, 0xF7, 0x6C, + 0x7B, 0x62, 0xE3, 0x35, 0xD1, 0xD2, 0x02, 0x98, 0x92, + 0xFE, 0x0B, 0x32, 0x9A, 0x7C, 0xCD, 0x0E, 0x95, 0x83, + 0x4A, 0xFB, 0xD8, 0xA2, 0x3B, 0x63, 0x9E, 0xDC, 0x0D, + 0xAC, 0xC9, 0x0C, 0xA1, 0xC6, 0x39, 0x7A, 0x30, 0x6E, + 0x9E, 0x64, 0x19 + }, + { /* group_aggnonce */ + 0x02, 0x23, 0x8A, 0x7D, 0x69, 0x2B, 0xED, 0x3E, 0x8B, + 0xBF, 0xF3, 0x70, 0x00, 0x84, 0xCF, 0x31, 0x2E, 0x4A, + 0x68, 0xC9, 0x43, 0xBD, 0x4D, 0xAC, 0x49, 0xDE, 0x5E, + 0xC0, 0xE0, 0xB9, 0x80, 0x04, 0x04, 0x03, 0x3E, 0x4B, + 0x5F, 0xE5, 0x83, 0xE4, 0xFA, 0x27, 0xD8, 0xE1, 0x31, + 0xEF, 0xAA, 0x9B, 0x53, 0xA3, 0x5B, 0x2B, 0x4E, 0x4A, + 0xF3, 0x2A, 0xAF, 0xFA, 0x2C, 0xD4, 0x4B, 0x7C, 0x65, + 0x91, 0x53, 0xE0 + }, + { /* group_pubnonce */ + 0x02, 0x23, 0x8A, 0x7D, 0x69, 0x2B, 0xED, 0x3E, 0x8B, + 0xBF, 0xF3, 0x70, 0x00, 0x84, 0xCF, 0x31, 0x2E, 0x4A, + 0x68, 0xC9, 0x43, 0xBD, 0x4D, 0xAC, 0x49, 0xDE, 0x5E, + 0xC0, 0xE0, 0xB9, 0x80, 0x04, 0x04, 0x02, 0x64, 0x37, + 0x8F, 0xFC, 0x31, 0xA6, 0x60, 0x21, 0x14, 0x3C, 0xAC, + 0x54, 0x11, 0xF5, 0xB6, 0xCF, 0xD2, 0x3A, 0xD6, 0x00, + 0x06, 0x7E, 0x7D, 0xE6, 0x50, 0xE0, 0x0B, 0xCA, 0xB4, + 0x7F, 0x8E, 0x92 + }, + { /* group_psig */ + 0x33, 0x52, 0xF6, 0x5D, 0x39, 0x6E, 0xBE, 0xF7, 0x31, + 0x90, 0xA9, 0x35, 0x4C, 0x85, 0xC6, 0x24, 0x90, 0x62, + 0xB3, 0xE2, 0x42, 0x6B, 0x6D, 0xD3, 0x7C, 0x16, 0x1D, + 0x77, 0xA1, 0xCE, 0x9F, 0x48 + }, + { /* pubshares, participant 1 first */ + { + 0x03, 0xCF, 0x1A, 0xCF, 0x20, 0x52, 0x24, + 0xA3, 0xF2, 0xC1, 0xDA, 0xA9, 0xB8, 0x18, + 0xCB, 0x19, 0x40, 0x7A, 0x57, 0x5A, 0xDB, + 0xA6, 0xBA, 0xF3, 0xBF, 0x98, 0x61, 0xD0, + 0x4E, 0xF1, 0xBB, 0xA1, 0x61 + }, + { + 0x02, 0x06, 0x41, 0x0D, 0xB9, 0xF0, 0x56, + 0x40, 0x0C, 0x87, 0xCC, 0xA1, 0x6E, 0x64, + 0x33, 0x56, 0x1D, 0x97, 0x26, 0x8E, 0x41, + 0xBB, 0x8F, 0xE5, 0x9C, 0x10, 0x62, 0x25, + 0x01, 0x01, 0x54, 0xEA, 0x5E + }, + { + 0x03, 0x99, 0xFE, 0xA6, 0x89, 0xA7, 0x76, + 0x3C, 0x56, 0xAB, 0xD1, 0x6F, 0x09, 0x10, + 0x6C, 0x98, 0xBB, 0xE1, 0x5D, 0x7C, 0x24, + 0xCC, 0xFA, 0x62, 0x1D, 0xE1, 0x41, 0x56, + 0x9D, 0x7D, 0x6F, 0x3D, 0x8C + }, + { + 0x03, 0xF3, 0xE2, 0x1D, 0x56, 0xB8, 0x58, + 0xF7, 0xEC, 0xB2, 0xFC, 0xDD, 0xE9, 0xBD, + 0x8E, 0xF3, 0x11, 0xFC, 0x7A, 0x30, 0x1D, + 0xC5, 0xC7, 0x93, 0x38, 0xE3, 0x33, 0xE6, + 0xB7, 0xB3, 0x7B, 0x30, 0x59 + }, + { + 0x02, 0xD2, 0xD0, 0x3A, 0xA2, 0x7D, 0xA0, + 0xDC, 0x27, 0x4B, 0x9E, 0x5C, 0x38, 0x82, + 0xEB, 0x56, 0xE6, 0xFF, 0xFA, 0xA2, 0x93, + 0x28, 0x06, 0x29, 0x59, 0xC5, 0x40, 0x12, + 0xD0, 0xE0, 0xD3, 0xA7, 0x91 + }, + { + 0x03, 0x7C, 0xAD, 0x0A, 0x4E, 0xDE, 0x9B, + 0x40, 0xCB, 0x01, 0x05, 0x6E, 0xE5, 0xC4, + 0xCB, 0x2F, 0x0B, 0xC4, 0xBF, 0x3D, 0xF9, + 0x15, 0x2A, 0x10, 0xBA, 0x86, 0x15, 0x3A, + 0xC6, 0x11, 0xA1, 0xA6, 0x49 + }, + { + 0x02, 0xDA, 0xC2, 0x7D, 0x4C, 0xE5, 0x79, + 0x0A, 0xEC, 0xDF, 0x3D, 0xA1, 0x4D, 0xB3, + 0x6B, 0xD3, 0xF3, 0x51, 0x02, 0x21, 0x21, + 0x6B, 0x2D, 0xA1, 0xA6, 0x53, 0x41, 0xE8, + 0x96, 0xFC, 0x2F, 0x7F, 0x86 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x02, 0xC3, 0x83, 0xC7, 0xC1, 0x70, 0xC4, + 0x97, 0x8E, 0x73, 0x35, 0x4C, 0x45, 0x6B, + 0x62, 0x37, 0x24, 0x93, 0xFF, 0x8A, 0x7E, + 0x7B, 0xEB, 0x1E, 0x94, 0xFE, 0xD8, 0xEE, + 0x7A, 0x3A, 0x85, 0x41, 0x03, 0x03, 0x34, + 0x99, 0x00, 0x67, 0xB5, 0x4F, 0x7D, 0x2E, + 0x33, 0x62, 0x48, 0x44, 0xA7, 0x57, 0x7E, + 0x41, 0x01, 0xD4, 0x49, 0x34, 0x18, 0x31, + 0x59, 0xD3, 0xE8, 0xCA, 0x0B, 0x3D, 0xEF, + 0x63, 0x70, 0x43 + }, + { + 0x02, 0xCD, 0x94, 0x06, 0x1E, 0x3A, 0x78, + 0xD7, 0x4C, 0xE1, 0x27, 0xD1, 0x7E, 0x03, + 0x6C, 0x0B, 0xF4, 0x0C, 0xA3, 0x9C, 0xFD, + 0x5B, 0x6A, 0x44, 0x21, 0xC6, 0x28, 0x69, + 0x04, 0x61, 0xDC, 0x6E, 0xEA, 0x03, 0x3D, + 0x26, 0x45, 0x8A, 0x05, 0xC2, 0x90, 0xE9, + 0x15, 0x9D, 0xFC, 0x34, 0x60, 0x34, 0x90, + 0x12, 0xB1, 0x7E, 0x70, 0x23, 0xAD, 0x66, + 0xC6, 0x26, 0x35, 0x47, 0x7C, 0xE4, 0x06, + 0xB1, 0xFB, 0xB8 + }, + { + 0x02, 0x5F, 0x29, 0x58, 0xDC, 0xF5, 0xD2, + 0xC6, 0x8D, 0xA3, 0x0C, 0x58, 0x60, 0xD7, + 0xDA, 0xF5, 0x02, 0x29, 0x0B, 0xCA, 0xB6, + 0x4F, 0xE4, 0xA8, 0x2B, 0x96, 0x7E, 0xFC, + 0x18, 0x08, 0x64, 0x86, 0xB0, 0x02, 0x9B, + 0xBF, 0x4D, 0x31, 0x98, 0x74, 0x49, 0x91, + 0x40, 0x49, 0x6C, 0x26, 0x22, 0xCA, 0xA8, + 0x38, 0x0F, 0x3B, 0x18, 0xA0, 0x47, 0x22, + 0xCD, 0x57, 0x32, 0x71, 0x8E, 0xB4, 0x07, + 0x19, 0x13, 0x3D + }, + { + 0x02, 0x6A, 0x11, 0xE1, 0x13, 0xCB, 0xB2, + 0x33, 0x88, 0x8E, 0xD7, 0xD5, 0xC3, 0x3F, + 0xA6, 0x18, 0x82, 0x37, 0x4D, 0xAD, 0xE1, + 0x89, 0x92, 0x96, 0xFA, 0x09, 0x4E, 0x2A, + 0x99, 0x53, 0x47, 0xDF, 0xDD, 0x02, 0xD0, + 0x04, 0x8D, 0x3D, 0xEF, 0x11, 0xC1, 0x95, + 0x89, 0xB6, 0x1A, 0xD9, 0xD0, 0x3B, 0xB7, + 0xBC, 0x1A, 0x66, 0xE5, 0x85, 0x53, 0x45, + 0x9C, 0xE0, 0x9F, 0xEA, 0xFC, 0xED, 0xA1, + 0x01, 0x6B, 0xCB + }, + { + 0x02, 0xB4, 0x22, 0x40, 0x46, 0xAE, 0x58, + 0x27, 0x50, 0x80, 0x26, 0xCC, 0xD8, 0x21, + 0x75, 0x47, 0x22, 0x5A, 0xB3, 0x61, 0x5A, + 0xC2, 0x57, 0x3D, 0x7F, 0xD8, 0xA2, 0x7E, + 0xDF, 0x20, 0xAD, 0x40, 0x0D, 0x02, 0x39, + 0x08, 0x3E, 0xEA, 0xCE, 0x02, 0xF4, 0x2F, + 0xA4, 0x2B, 0xED, 0xE3, 0x32, 0xC8, 0xB3, + 0xCC, 0x5A, 0x82, 0x63, 0xD0, 0xF4, 0x7C, + 0xC7, 0x0F, 0x44, 0x66, 0xE1, 0xF6, 0x9D, + 0x4C, 0x56, 0x2A + }, + { + 0x02, 0x85, 0x4B, 0x4B, 0x39, 0x28, 0x5A, + 0x09, 0xDA, 0xAE, 0x55, 0x51, 0x8F, 0xFA, + 0x95, 0x2E, 0x2F, 0xBB, 0x76, 0x75, 0x7F, + 0x53, 0xA0, 0x31, 0x99, 0xAB, 0xF2, 0xB0, + 0xD6, 0x60, 0x58, 0xDC, 0xA5, 0x02, 0x17, + 0x4B, 0x4C, 0x61, 0x41, 0xAD, 0xDF, 0x0E, + 0x0A, 0xDA, 0xAB, 0xC9, 0xC9, 0xE6, 0x29, + 0x5D, 0xB8, 0x17, 0xD7, 0x65, 0x48, 0xFF, + 0x29, 0x92, 0x3F, 0xEF, 0x68, 0x1C, 0xD4, + 0x4B, 0x03, 0x60 + }, + { + 0x02, 0x90, 0x6C, 0x68, 0x5A, 0xA6, 0x1A, + 0xB5, 0x79, 0x6F, 0x92, 0x80, 0xFD, 0xBE, + 0xC0, 0xD9, 0x31, 0x07, 0x66, 0xF1, 0xC6, + 0xEC, 0x5F, 0x24, 0xA4, 0x12, 0x50, 0x68, + 0x88, 0x3B, 0x36, 0xAF, 0xCC, 0x02, 0xD6, + 0x5C, 0x81, 0x28, 0x21, 0x21, 0xDC, 0xA4, + 0x04, 0x95, 0xA7, 0x1F, 0xDA, 0x8D, 0x71, + 0x05, 0x2C, 0x3A, 0xFD, 0x4B, 0xA9, 0xEA, + 0x9F, 0x4C, 0x3B, 0x13, 0xB8, 0x2E, 0xBF, + 0xEE, 0x98, 0xE5 + }, + }, + { /* signature shares, the first t participants */ + { + 0x77, 0xD0, 0x8A, 0x2E, 0x74, 0x58, 0x2A, + 0xD1, 0xA2, 0x38, 0x38, 0xF4, 0xA9, 0x7D, + 0x69, 0xFD, 0x51, 0x07, 0x33, 0xB7, 0x21, + 0x61, 0xE5, 0x0F, 0xA0, 0x85, 0x70, 0x49, + 0xD9, 0x0E, 0xDA, 0x4B + }, + { + 0x44, 0x91, 0x72, 0xAA, 0x78, 0x6F, 0xD7, + 0xBF, 0xD0, 0x24, 0x1E, 0x35, 0x74, 0xB8, + 0x0B, 0x62, 0xA9, 0xAF, 0x0A, 0xC2, 0x38, + 0xA2, 0xB6, 0x74, 0x6E, 0x6A, 0x2C, 0x39, + 0xA1, 0x03, 0x66, 0x58 + }, + { + 0xE7, 0x84, 0x47, 0xCA, 0xA6, 0x6E, 0x37, + 0x1E, 0xB2, 0x81, 0x42, 0xA9, 0xA1, 0xDA, + 0xE8, 0xDF, 0x34, 0x0F, 0xB9, 0xC3, 0x18, + 0x8E, 0xCE, 0x3D, 0x97, 0x2D, 0xF0, 0x89, + 0xFE, 0x16, 0x6F, 0xAC + }, + { + 0xAE, 0x97, 0xA1, 0x88, 0x5F, 0x0B, 0xBA, + 0x4B, 0x40, 0x7C, 0x90, 0x03, 0x24, 0x8B, + 0x41, 0x01, 0x59, 0xD2, 0x2A, 0xC5, 0x43, + 0xAD, 0x37, 0xF3, 0x8C, 0xC3, 0x40, 0xD7, + 0x84, 0x0F, 0x5E, 0xC1 + }, + }, + }, + { + "5of9", 9, 5, 9, 126, + { /* one seed per (t-1)-subset, in rank order */ + { + 0x23, 0x0E, 0x9B, 0x33, 0xB2, 0x85, 0x32, + 0xDA, 0x36, 0x93, 0xD9, 0x25, 0xDD, 0x97, + 0xB8, 0x18, 0xF1, 0x4B, 0x3B, 0x64, 0x3C, + 0x87, 0xB4, 0xA4, 0xD5, 0x4F, 0x53, 0xC8, + 0xEA, 0xF7, 0x53, 0x0C + }, + { + 0x60, 0xE3, 0xEA, 0xC8, 0x7F, 0x2C, 0x66, + 0x62, 0xE7, 0xD8, 0xAA, 0x81, 0x2D, 0x4F, + 0x4D, 0x2D, 0xBB, 0x22, 0x94, 0x06, 0x61, + 0xD7, 0x1B, 0x7F, 0x99, 0xC8, 0xDE, 0xF2, + 0x3B, 0x8A, 0x14, 0x45 + }, + { + 0x9F, 0x55, 0x02, 0xF0, 0xD3, 0x26, 0x5C, + 0xEA, 0x8E, 0x87, 0xEB, 0x01, 0x22, 0x0F, + 0x7E, 0x8C, 0x8C, 0x88, 0x27, 0x1B, 0xF0, + 0x4E, 0x15, 0x51, 0x1E, 0xB5, 0x4B, 0x78, + 0x5B, 0xD9, 0xE0, 0xA6 + }, + { + 0x17, 0x98, 0x3A, 0x0E, 0x99, 0xFB, 0x07, + 0x94, 0xDA, 0x78, 0x71, 0x98, 0xD2, 0x5E, + 0xDD, 0x79, 0xD9, 0xC7, 0xD8, 0xE3, 0x2C, + 0x2D, 0xB8, 0xB8, 0x35, 0x25, 0xE7, 0x2A, + 0x0D, 0xDE, 0x82, 0x8A + }, + { + 0x49, 0x9D, 0x33, 0x12, 0x2E, 0x61, 0xAB, + 0xC6, 0x62, 0xA0, 0x1D, 0xE6, 0x38, 0xED, + 0xAD, 0xF2, 0x9E, 0x43, 0xF7, 0x37, 0x10, + 0x7F, 0x07, 0x20, 0x6A, 0xC0, 0xF3, 0x5A, + 0x6B, 0xD2, 0xE3, 0x6F + }, + { + 0x62, 0xA7, 0x03, 0xB0, 0x02, 0x3F, 0xCB, + 0x0A, 0xC5, 0x86, 0x7D, 0xC5, 0x94, 0x82, + 0x82, 0x32, 0xB8, 0x3C, 0xA0, 0xC8, 0xB9, + 0xE2, 0x3C, 0xB2, 0x1F, 0x0D, 0x1B, 0x94, + 0x85, 0x6F, 0x43, 0x02 + }, + { + 0xA7, 0x2D, 0x69, 0xA6, 0x2F, 0x45, 0x62, + 0xC3, 0xF4, 0x5E, 0x65, 0x83, 0x64, 0x5B, + 0x43, 0x56, 0x34, 0x91, 0x8F, 0x40, 0xFB, + 0xBE, 0x6A, 0xF0, 0x0F, 0x8F, 0x1F, 0x8C, + 0x9A, 0x28, 0x1F, 0xE7 + }, + { + 0x02, 0xF3, 0xA6, 0x01, 0x28, 0xBD, 0x1C, + 0xD1, 0xC6, 0x9A, 0xB6, 0x23, 0x86, 0x8C, + 0x61, 0x51, 0x35, 0x1A, 0xF9, 0x32, 0xFE, + 0xD6, 0x69, 0x4F, 0x1C, 0x99, 0x8A, 0xBD, + 0x78, 0xC0, 0xBB, 0xC4 + }, + { + 0x29, 0x8B, 0x8E, 0x02, 0xBB, 0x26, 0x89, + 0x98, 0xF1, 0x49, 0x79, 0x92, 0x63, 0xFF, + 0xE6, 0xDF, 0x3E, 0x6C, 0x7C, 0xB8, 0x0C, + 0xE6, 0x22, 0x2B, 0x11, 0x4D, 0xE8, 0x8C, + 0x9B, 0x45, 0x31, 0x7A + }, + { + 0x59, 0x98, 0x73, 0x79, 0xCA, 0xC0, 0xC8, + 0x52, 0xF2, 0x8D, 0x01, 0x0E, 0x73, 0x06, + 0x04, 0x2F, 0x7C, 0xBE, 0x03, 0x20, 0x61, + 0xA8, 0xD7, 0x81, 0x32, 0x92, 0x14, 0xD3, + 0x45, 0xCF, 0xC9, 0x54 + }, + { + 0x93, 0xFE, 0xE9, 0xEB, 0xE0, 0x44, 0xA5, + 0xAC, 0x5A, 0xA7, 0x9B, 0xDE, 0x44, 0xDB, + 0x58, 0xBB, 0x68, 0x52, 0x03, 0xA6, 0x02, + 0x5C, 0x8C, 0xD1, 0xE1, 0x18, 0x93, 0x1D, + 0x1B, 0x44, 0x6C, 0xFD + }, + { + 0xE4, 0x46, 0xAE, 0x9C, 0x96, 0xF8, 0xE1, + 0x2B, 0xC8, 0x9E, 0xA4, 0x3B, 0x73, 0x3C, + 0x3E, 0x08, 0x49, 0x7D, 0x01, 0x68, 0xD4, + 0xA5, 0x38, 0x2D, 0x6C, 0xEA, 0x09, 0xE1, + 0x24, 0x7C, 0xCA, 0x63 + }, + { + 0x00, 0x3D, 0xAA, 0xD5, 0xE9, 0xA9, 0x99, + 0xFF, 0x1D, 0x01, 0x2C, 0xCD, 0xAC, 0x8B, + 0x28, 0x5D, 0x2B, 0xF6, 0x84, 0x9C, 0x0D, + 0xD8, 0x5D, 0x6C, 0x03, 0x2F, 0x72, 0xE0, + 0xE6, 0xAA, 0x0A, 0x2E + }, + { + 0x2B, 0x9F, 0x75, 0x2D, 0xA6, 0x67, 0x9D, + 0x1D, 0x0E, 0x5C, 0x38, 0xC1, 0x95, 0x5E, + 0xB1, 0x48, 0x69, 0x67, 0xAE, 0x8A, 0x09, + 0x39, 0x1F, 0xDC, 0xEB, 0x85, 0xD8, 0xFE, + 0xF3, 0x35, 0x29, 0xC8 + }, + { + 0x97, 0xFB, 0x9C, 0x03, 0xAE, 0xAC, 0x9E, + 0x8B, 0x63, 0xB5, 0x51, 0x47, 0x69, 0x43, + 0x80, 0x05, 0x37, 0x8E, 0x6B, 0x5D, 0x59, + 0xE1, 0xA3, 0xE4, 0x95, 0x69, 0x02, 0x73, + 0x5C, 0xCA, 0x20, 0xC6 + }, + { + 0x78, 0x64, 0xE1, 0x5F, 0x5D, 0x53, 0x9E, + 0xC0, 0x2A, 0xE1, 0x28, 0xE3, 0x89, 0x84, + 0xBF, 0xA4, 0xFF, 0x7B, 0x18, 0x52, 0xFD, + 0x22, 0x27, 0xDD, 0xAA, 0x09, 0x1C, 0x80, + 0xA5, 0x25, 0x94, 0x8A + }, + { + 0x98, 0x29, 0xE6, 0xF9, 0x35, 0xBD, 0xB2, + 0x51, 0xA5, 0xAF, 0xFF, 0x8F, 0x16, 0x01, + 0x22, 0x44, 0xCB, 0x5F, 0xD5, 0xC3, 0x23, + 0x9B, 0xC9, 0xCA, 0xFE, 0x20, 0x0B, 0x9D, + 0x06, 0xE9, 0x5E, 0x2B + }, + { + 0x3D, 0x8B, 0x79, 0xB4, 0x39, 0xB4, 0xED, + 0xED, 0x89, 0x33, 0xAF, 0xF2, 0x5F, 0x76, + 0x11, 0xC4, 0x7B, 0xBE, 0xE8, 0x40, 0x64, + 0x75, 0x54, 0x98, 0x8E, 0xF4, 0x34, 0xED, + 0x91, 0x52, 0xED, 0x51 + }, + { + 0x98, 0x4E, 0x08, 0x99, 0x2D, 0x70, 0x47, + 0xE6, 0xC9, 0x87, 0x58, 0xE6, 0x85, 0x49, + 0x99, 0x8C, 0xAA, 0x6E, 0xF3, 0x87, 0xF4, + 0x13, 0x38, 0xE2, 0x96, 0x0F, 0x19, 0xC7, + 0x8F, 0x39, 0xEB, 0x40 + }, + { + 0x1C, 0xEC, 0x7F, 0x4B, 0xA2, 0x60, 0x76, + 0x27, 0xC0, 0xA8, 0x23, 0xF6, 0x9E, 0x5E, + 0xE7, 0xD2, 0xFF, 0x28, 0xF8, 0x76, 0xA1, + 0xBF, 0x41, 0xE8, 0xD1, 0xFA, 0x2A, 0xAD, + 0x0C, 0x30, 0x38, 0x57 + }, + { + 0x8E, 0x5D, 0xFB, 0x6D, 0xDB, 0x14, 0x41, + 0x46, 0x76, 0x31, 0x67, 0xB5, 0x44, 0x10, + 0xCB, 0xD3, 0x5D, 0x6C, 0xD6, 0xD1, 0xFF, + 0x43, 0xC9, 0xD5, 0xB9, 0x18, 0xB3, 0xEE, + 0xEA, 0x1E, 0x95, 0xCE + }, + { + 0x9D, 0xA0, 0xEB, 0x5A, 0xE3, 0x86, 0xD8, + 0x95, 0x70, 0x55, 0x7C, 0x7D, 0xFB, 0xDC, + 0xB3, 0xD3, 0xDE, 0xED, 0xDA, 0xBB, 0x0C, + 0x2B, 0xDD, 0x4F, 0xC6, 0xF3, 0x22, 0xEF, + 0x6F, 0x3A, 0x92, 0xBA + }, + { + 0xBA, 0xBF, 0x92, 0x69, 0x62, 0x11, 0x41, + 0x4B, 0xD2, 0xBE, 0xF1, 0x10, 0x64, 0xD8, + 0xCB, 0x6A, 0x34, 0x8B, 0x2E, 0x5D, 0x60, + 0x95, 0xDA, 0x4C, 0xC4, 0xF5, 0x6D, 0x99, + 0x10, 0x0B, 0x88, 0xF5 + }, + { + 0x6B, 0x82, 0x2B, 0x44, 0x80, 0xE7, 0xD3, + 0x72, 0x5F, 0x1B, 0xBB, 0x91, 0xFE, 0xA7, + 0xFF, 0xA3, 0xF1, 0x2C, 0xE4, 0xF0, 0xD9, + 0x8E, 0xCF, 0x52, 0x20, 0x23, 0x05, 0xEC, + 0xBA, 0xF1, 0xD3, 0x83 + }, + { + 0x36, 0x1F, 0x1A, 0xF7, 0xA6, 0x52, 0x35, + 0x0E, 0x31, 0x36, 0xA5, 0x41, 0x0D, 0x66, + 0xCC, 0x45, 0xC5, 0x0A, 0xD0, 0x6F, 0x31, + 0x45, 0xA1, 0x48, 0xC8, 0x10, 0x96, 0xE1, + 0xB1, 0x39, 0x18, 0xA5 + }, + { + 0xE9, 0x5D, 0x89, 0xF7, 0x31, 0x86, 0xAC, + 0xA6, 0xF3, 0x4A, 0x74, 0xDE, 0x2C, 0x35, + 0x2E, 0xB7, 0x8F, 0x5A, 0x82, 0x0C, 0xDB, + 0x6A, 0xAA, 0xE4, 0xD7, 0xE0, 0x5E, 0xE6, + 0xB5, 0xE0, 0x07, 0x0B + }, + { + 0x3C, 0x31, 0x64, 0x3C, 0x17, 0x0E, 0x06, + 0x09, 0x1B, 0xEE, 0xD8, 0xCC, 0x4A, 0x2C, + 0x57, 0xEF, 0xC7, 0x96, 0xE5, 0xE1, 0xCE, + 0x52, 0x10, 0xA0, 0xD7, 0xB3, 0xC9, 0x70, + 0x92, 0x2E, 0xFC, 0x85 + }, + { + 0xCE, 0x63, 0xCC, 0xC6, 0x50, 0x13, 0x8F, + 0xCA, 0x5D, 0x6D, 0x13, 0xA8, 0xBE, 0x9B, + 0x7D, 0x49, 0x4B, 0x4C, 0x5C, 0x2D, 0x3E, + 0xC6, 0x0C, 0x9B, 0x8B, 0x55, 0x57, 0xD0, + 0xBC, 0x87, 0x29, 0xC3 + }, + { + 0x8E, 0x9E, 0x6E, 0x6C, 0xA3, 0xB5, 0xB6, + 0xBD, 0xE8, 0xCA, 0x58, 0xE7, 0x1C, 0xD7, + 0x85, 0xE3, 0x4A, 0x14, 0x34, 0x61, 0x24, + 0xBF, 0xEA, 0x3F, 0x90, 0x3B, 0x7F, 0xF2, + 0x27, 0xE5, 0xF8, 0x92 + }, + { + 0xDD, 0x8F, 0x83, 0xE9, 0xB8, 0xE7, 0x5E, + 0x2D, 0x90, 0xA7, 0xB6, 0x36, 0x95, 0x09, + 0x15, 0xB7, 0x82, 0xEC, 0x72, 0x90, 0x9A, + 0x88, 0xB9, 0xA0, 0x19, 0x58, 0xF9, 0x20, + 0x20, 0x57, 0xDE, 0x54 + }, + { + 0x10, 0x59, 0x01, 0xD4, 0xBF, 0xED, 0x32, + 0x9D, 0xB8, 0x75, 0x08, 0x60, 0x47, 0x9C, + 0x41, 0x99, 0x0C, 0xFC, 0x4D, 0x44, 0xF2, + 0x2E, 0x74, 0x87, 0xFC, 0x49, 0x5F, 0x4B, + 0xC0, 0x50, 0xBC, 0x91 + }, + { + 0xA8, 0xE7, 0xAE, 0x2E, 0x56, 0xBC, 0x39, + 0x1D, 0xE8, 0xAC, 0xA4, 0xCE, 0xC0, 0x39, + 0x9E, 0x70, 0xC7, 0x28, 0xF9, 0xD4, 0x54, + 0xE6, 0x85, 0xE3, 0x4F, 0xDE, 0x3F, 0xEE, + 0x5A, 0x2C, 0x25, 0x88 + }, + { + 0xFF, 0xDE, 0x7A, 0xBD, 0xD3, 0xCD, 0x73, + 0x8F, 0x60, 0x51, 0x1C, 0x1C, 0xF8, 0xA2, + 0x9A, 0x34, 0x97, 0xCD, 0x48, 0x64, 0xBE, + 0x8C, 0x2C, 0xD1, 0x57, 0xEA, 0x60, 0x17, + 0x98, 0x76, 0x86, 0xFC + }, + { + 0xF4, 0xCF, 0xC3, 0xE6, 0x61, 0xFE, 0xAA, + 0x7A, 0x10, 0x4D, 0x14, 0xD1, 0x52, 0xF0, + 0x45, 0x2A, 0x91, 0x63, 0xB0, 0x60, 0xEA, + 0x34, 0xE0, 0x85, 0x3F, 0xFB, 0xA2, 0x29, + 0x32, 0x61, 0xB2, 0xE8 + }, + { + 0x18, 0xAC, 0x03, 0x00, 0x37, 0xBB, 0xA2, + 0x13, 0x82, 0x73, 0x8B, 0x63, 0x47, 0xFC, + 0xEB, 0xAB, 0x11, 0x20, 0x13, 0xAE, 0x9D, + 0x60, 0xCD, 0x13, 0xD7, 0x3E, 0x76, 0xF9, + 0x20, 0x8C, 0x19, 0xA8 + }, + { + 0x4B, 0xD5, 0x0B, 0x1A, 0xD3, 0xAE, 0xB1, + 0x1D, 0x50, 0x84, 0x5E, 0xAC, 0x19, 0xEA, + 0x0F, 0xE7, 0xBE, 0xB6, 0xC6, 0xA1, 0x37, + 0xEB, 0x5B, 0x52, 0x33, 0x98, 0x01, 0xEC, + 0x49, 0xD0, 0xD1, 0x98 + }, + { + 0x0C, 0xF6, 0xE9, 0xBD, 0x81, 0xA7, 0x37, + 0x59, 0x6F, 0x39, 0xE9, 0x8F, 0x5F, 0x52, + 0xAF, 0xEA, 0x20, 0x4B, 0x02, 0xF7, 0xB5, + 0x37, 0x56, 0x30, 0x0C, 0x80, 0x61, 0x7E, + 0x81, 0xC2, 0xD8, 0xC0 + }, + { + 0xDA, 0x7A, 0x2F, 0x9D, 0x23, 0x7F, 0xEA, + 0x1C, 0x08, 0xFA, 0xB0, 0x74, 0x81, 0x91, + 0x7D, 0x4E, 0x03, 0x96, 0x4F, 0x5D, 0x84, + 0xDB, 0xE5, 0x71, 0xB2, 0xB8, 0x5E, 0xFA, + 0xEC, 0x9B, 0xBE, 0x63 + }, + { + 0x33, 0xB3, 0xE4, 0x22, 0xF4, 0x08, 0x36, + 0x21, 0x3F, 0x08, 0xD3, 0xA8, 0x48, 0x74, + 0x05, 0xED, 0x5C, 0xB4, 0x7B, 0x02, 0xE8, + 0x4C, 0xD9, 0xBA, 0x80, 0x29, 0x67, 0xAB, + 0x3E, 0x5A, 0xC0, 0xBB + }, + { + 0x0F, 0xCB, 0xF1, 0x97, 0xB6, 0xF6, 0xF0, + 0xB4, 0x71, 0x0D, 0x0C, 0xE5, 0x58, 0x58, + 0x0C, 0x7A, 0x45, 0x38, 0x2E, 0x40, 0xD6, + 0x3C, 0xD8, 0x29, 0x82, 0xEF, 0xAB, 0x42, + 0x9B, 0x53, 0x2B, 0xE5 + }, + { + 0x34, 0x29, 0x83, 0xBE, 0x97, 0xF3, 0xDD, + 0x45, 0x0D, 0xE5, 0x23, 0x5A, 0xDC, 0xE7, + 0x01, 0xF4, 0xA2, 0x15, 0x30, 0x2C, 0x11, + 0x83, 0x25, 0x8E, 0x23, 0xA8, 0xD4, 0xFC, + 0x28, 0x40, 0x55, 0xFB + }, + { + 0xEE, 0x04, 0x8E, 0x45, 0xFF, 0xEA, 0x05, + 0x07, 0x9D, 0x1A, 0xC3, 0xD5, 0x61, 0x7C, + 0x70, 0x19, 0x43, 0x3D, 0xBF, 0xB0, 0x65, + 0x4A, 0x89, 0xC2, 0x7C, 0xE3, 0xF2, 0x30, + 0xE0, 0x12, 0x66, 0x60 + }, + { + 0x40, 0x12, 0x00, 0x7A, 0x60, 0x75, 0xC1, + 0x69, 0xFD, 0x38, 0xDF, 0xB7, 0xD8, 0x26, + 0xE6, 0x11, 0xF4, 0xAA, 0xDB, 0x48, 0x99, + 0x14, 0x3C, 0x28, 0xBB, 0xD0, 0x14, 0x67, + 0x3E, 0x4F, 0xE0, 0x43 + }, + { + 0x08, 0x0A, 0x02, 0x3C, 0xC6, 0xC1, 0x87, + 0x04, 0xDE, 0xF5, 0x69, 0x49, 0xEF, 0xEA, + 0x00, 0xDA, 0xEB, 0x62, 0x70, 0x84, 0x3D, + 0x3C, 0x1B, 0xCA, 0xD2, 0x04, 0xD0, 0x74, + 0x86, 0xF8, 0x18, 0x02 + }, + { + 0x12, 0x40, 0x66, 0xA0, 0xAB, 0xB4, 0x33, + 0x85, 0xB5, 0xCF, 0x4D, 0xE3, 0xAB, 0x1E, + 0xA5, 0x54, 0x82, 0x2F, 0xCA, 0x27, 0x2E, + 0xD9, 0xB5, 0x69, 0xC3, 0x29, 0x98, 0x84, + 0xFA, 0xD6, 0x4C, 0xD9 + }, + { + 0x31, 0x02, 0x15, 0x0A, 0x99, 0xAF, 0x4B, + 0x28, 0x18, 0xE0, 0x92, 0x20, 0x65, 0xA5, + 0xE0, 0x43, 0xB1, 0x26, 0xA1, 0xC4, 0x10, + 0x04, 0x73, 0xC7, 0xB6, 0xE2, 0x1A, 0x8E, + 0x6F, 0x65, 0xD7, 0x89 + }, + { + 0x32, 0x74, 0x56, 0xFF, 0xB1, 0xC2, 0x96, + 0x97, 0x98, 0xBC, 0x70, 0xA8, 0x09, 0x92, + 0xB2, 0xAC, 0x45, 0xF4, 0x2B, 0x56, 0x02, + 0x1E, 0x52, 0xDC, 0xE9, 0xB4, 0x20, 0xB6, + 0x33, 0x22, 0xC0, 0x20 + }, + { + 0x64, 0xBA, 0x69, 0x8B, 0x5A, 0xB6, 0x56, + 0x31, 0x2E, 0xCC, 0xEB, 0x6C, 0x36, 0x1B, + 0xAB, 0x45, 0xB5, 0xEC, 0x3B, 0x65, 0x64, + 0x90, 0x41, 0xA8, 0x42, 0xA0, 0x32, 0x9C, + 0x34, 0x24, 0x7F, 0x4E + }, + { + 0x4A, 0x08, 0x24, 0x67, 0xB8, 0x96, 0xBB, + 0x69, 0xC2, 0xCD, 0x80, 0x60, 0xF3, 0x03, + 0x08, 0x05, 0x48, 0x92, 0x69, 0x7B, 0x8B, + 0xBA, 0xD9, 0x03, 0x67, 0xDA, 0x9C, 0x8A, + 0x81, 0xC2, 0x9B, 0xF4 + }, + { + 0xC0, 0x93, 0x9C, 0x2E, 0x7C, 0x44, 0xC0, + 0xB3, 0x93, 0x3D, 0xBE, 0x57, 0xCE, 0x16, + 0x99, 0x72, 0x63, 0xF6, 0xAB, 0x2D, 0x58, + 0xB9, 0x27, 0x1D, 0xE7, 0xBE, 0x11, 0xE7, + 0xED, 0x5A, 0xBC, 0x45 + }, + { + 0x24, 0x51, 0xEE, 0xF4, 0x73, 0xF6, 0x1E, + 0x33, 0x43, 0x61, 0x75, 0x4D, 0x03, 0xC0, + 0x2C, 0x57, 0x73, 0xFD, 0x70, 0x56, 0xD6, + 0x94, 0xF4, 0xA0, 0xB5, 0x9A, 0xEE, 0x59, + 0x17, 0xE1, 0x83, 0x4B + }, + { + 0x83, 0x4C, 0xD7, 0x0A, 0xED, 0xC0, 0xB6, + 0x07, 0xCC, 0x71, 0x8B, 0x3F, 0xB9, 0xC4, + 0x99, 0x27, 0xE9, 0x31, 0x04, 0x4F, 0x16, + 0x6F, 0x18, 0xAD, 0x2C, 0xFA, 0xD2, 0x30, + 0xD9, 0xE8, 0xE5, 0x3C + }, + { + 0xD0, 0xC4, 0xBD, 0x88, 0x81, 0x92, 0x22, + 0x1C, 0x56, 0xCD, 0xE0, 0x36, 0xB9, 0xFE, + 0x61, 0xFD, 0xA7, 0x02, 0x8F, 0xB1, 0x69, + 0xDF, 0x1A, 0x68, 0xB8, 0xBF, 0x89, 0xC6, + 0x6E, 0x65, 0x95, 0x50 + }, + { + 0xF8, 0x98, 0xB9, 0x65, 0xEF, 0x41, 0x55, + 0x96, 0xD8, 0x61, 0x82, 0x38, 0x33, 0xEB, + 0x5F, 0x88, 0xB9, 0x3B, 0x99, 0x3B, 0x31, + 0x40, 0x55, 0xB3, 0xCF, 0x62, 0x77, 0xB5, + 0x3B, 0x35, 0xA2, 0xEC + }, + { + 0x2D, 0xEB, 0x50, 0x66, 0xCB, 0xE8, 0x18, + 0x0D, 0x1D, 0x0D, 0x58, 0x5C, 0x64, 0xFB, + 0x35, 0x1C, 0x16, 0x63, 0xF0, 0x93, 0x56, + 0x24, 0x1A, 0xD5, 0x57, 0x81, 0x83, 0x6F, + 0xF6, 0x86, 0xC5, 0x2C + }, + { + 0x33, 0x95, 0xD6, 0xA7, 0x75, 0xA6, 0x7D, + 0x80, 0xA7, 0x1A, 0x9E, 0xEC, 0xC8, 0x0A, + 0xDB, 0x19, 0xC6, 0xCC, 0x4D, 0x6A, 0x17, + 0x25, 0x1E, 0x00, 0x19, 0x54, 0x07, 0x5C, + 0x5B, 0x96, 0x7D, 0x7C + }, + { + 0x90, 0xBE, 0xC2, 0x9F, 0x4C, 0x2E, 0xAC, + 0xB6, 0xBE, 0x42, 0x2F, 0x01, 0x74, 0xE4, + 0x55, 0x64, 0x1B, 0x50, 0x33, 0x16, 0x3D, + 0x62, 0x39, 0x55, 0xDE, 0xD7, 0x23, 0xD1, + 0x33, 0x25, 0xFE, 0x75 + }, + { + 0x47, 0x20, 0x7F, 0xA6, 0x7C, 0x4D, 0x18, + 0x29, 0x93, 0x1D, 0xBD, 0x7A, 0xBB, 0xCE, + 0x8B, 0xE1, 0x19, 0xB5, 0x19, 0xCC, 0x9D, + 0x6C, 0xC3, 0x4C, 0xDC, 0xF8, 0x06, 0xF3, + 0x7C, 0x55, 0x65, 0x59 + }, + { + 0xE9, 0x8B, 0xC5, 0x43, 0x39, 0x3C, 0x4A, + 0xA5, 0xBE, 0xB9, 0x5F, 0x8B, 0x83, 0x0D, + 0xC6, 0x5B, 0xB0, 0xC5, 0x59, 0x0D, 0xD9, + 0xE0, 0xCB, 0xF9, 0xDC, 0x6C, 0x0A, 0xD5, + 0xAB, 0xC5, 0x19, 0x01 + }, + { + 0xAF, 0x9A, 0x8B, 0xA3, 0x11, 0xC8, 0xB4, + 0xE5, 0x33, 0x62, 0xCA, 0x4D, 0xDD, 0x39, + 0xB9, 0x21, 0x67, 0x92, 0x01, 0x35, 0x81, + 0xA8, 0xED, 0x26, 0x6F, 0xE6, 0x4B, 0xB2, + 0xC2, 0x82, 0x0D, 0x9C + }, + { + 0xF7, 0xBB, 0x69, 0xDF, 0x72, 0xB9, 0x03, + 0xC6, 0x26, 0x64, 0xA5, 0x28, 0xF6, 0x48, + 0xBC, 0xF6, 0xBF, 0xBB, 0x28, 0x77, 0x25, + 0xB6, 0x45, 0xCF, 0xD2, 0x23, 0x98, 0xA6, + 0x2D, 0xBF, 0xF1, 0x30 + }, + { + 0xBA, 0xC0, 0xA6, 0x4D, 0x7D, 0xE2, 0x94, + 0x3F, 0xDB, 0x02, 0x37, 0x91, 0x4B, 0x9E, + 0x53, 0x3D, 0xA0, 0xA8, 0x3C, 0x84, 0x8F, + 0x92, 0x9A, 0x87, 0xC6, 0xCB, 0x1E, 0xA0, + 0xA1, 0x6B, 0x76, 0x79 + }, + { + 0xFA, 0xC8, 0xE7, 0x59, 0x5D, 0xD7, 0xFF, + 0x50, 0xD9, 0x11, 0x6D, 0xC3, 0x85, 0xD0, + 0x3D, 0x5D, 0x52, 0xE8, 0x6B, 0x6D, 0xE6, + 0xD4, 0x00, 0x96, 0x85, 0x70, 0x95, 0x48, + 0x07, 0x91, 0xEF, 0xAB + }, + { + 0x4C, 0x78, 0xF8, 0xF5, 0x7C, 0x5D, 0x28, + 0x3C, 0xFA, 0xAC, 0x1B, 0xE2, 0x11, 0x52, + 0x64, 0x75, 0x8A, 0xBD, 0x34, 0x4D, 0x62, + 0x6A, 0xFD, 0x5F, 0x23, 0xBA, 0xE4, 0xB5, + 0xDB, 0x3F, 0xC6, 0x4D + }, + { + 0xE9, 0xB2, 0xE8, 0x44, 0xC7, 0xF7, 0x0D, + 0x3D, 0xDF, 0xEE, 0x62, 0x6C, 0xC0, 0x03, + 0xFB, 0x22, 0x40, 0xDD, 0x88, 0xE9, 0x54, + 0x8C, 0x00, 0x25, 0x22, 0xE1, 0x37, 0x8A, + 0x0A, 0x76, 0x1D, 0x64 + }, + { + 0x74, 0x6D, 0x38, 0xD1, 0x54, 0x05, 0x64, + 0x19, 0x35, 0xFF, 0xDD, 0xA2, 0x19, 0x54, + 0x71, 0xCC, 0x2E, 0x5F, 0x94, 0xBF, 0x09, + 0x84, 0x14, 0x5E, 0xDD, 0x32, 0xD9, 0xD8, + 0xEE, 0x17, 0xB9, 0x21 + }, + { + 0x89, 0x50, 0xB2, 0x88, 0x53, 0x4C, 0xE7, + 0x5F, 0xC6, 0xB1, 0xBC, 0xF3, 0xBF, 0xB3, + 0xE3, 0xF7, 0xD0, 0x55, 0x6E, 0x5E, 0xAA, + 0x95, 0xFF, 0x8F, 0xFE, 0x03, 0xA4, 0x1A, + 0xE1, 0xF6, 0xB4, 0x0F + }, + { + 0x7A, 0x73, 0x16, 0x49, 0x46, 0x27, 0xC4, + 0x7D, 0x81, 0xA2, 0x64, 0x5D, 0x38, 0x87, + 0x2B, 0x59, 0x2D, 0xE0, 0xCB, 0x96, 0xD3, + 0x9B, 0xE6, 0x7D, 0x92, 0xF4, 0xCA, 0x44, + 0x56, 0x7F, 0xB2, 0xCC + }, + { + 0xD1, 0x1B, 0x30, 0xE5, 0x36, 0xEC, 0xD4, + 0x46, 0x3C, 0xDA, 0xEF, 0x40, 0xC9, 0xB8, + 0x25, 0x63, 0x29, 0x5E, 0x9F, 0x2B, 0x24, + 0xEF, 0xA8, 0x89, 0x8F, 0x6D, 0xB3, 0x8D, + 0xCC, 0x04, 0x78, 0x65 + }, + { + 0x80, 0x0D, 0x48, 0x89, 0xA1, 0x67, 0x96, + 0x67, 0x33, 0x22, 0x27, 0xB4, 0x7D, 0xF4, + 0x98, 0x22, 0xCA, 0xDD, 0x59, 0x13, 0x15, + 0x55, 0xE1, 0xB6, 0x9B, 0x6C, 0x7B, 0x70, + 0xC5, 0xCB, 0x90, 0x73 + }, + { + 0x4B, 0xAC, 0x09, 0x96, 0xA1, 0x79, 0x77, + 0xE9, 0x18, 0x81, 0xB2, 0xC4, 0x2F, 0xAB, + 0x8C, 0xC5, 0x65, 0xB3, 0x8F, 0x51, 0xAC, + 0x9D, 0x83, 0x35, 0x2D, 0x60, 0xA6, 0xE2, + 0x82, 0x50, 0xA7, 0x6D + }, + { + 0x20, 0xF7, 0xCF, 0xBD, 0x4C, 0xD1, 0xA1, + 0xAF, 0xED, 0x88, 0xC2, 0x73, 0xF1, 0xD4, + 0x96, 0x4A, 0x12, 0x38, 0x60, 0x75, 0x96, + 0xE6, 0x45, 0xEC, 0x8B, 0x2E, 0x73, 0x5B, + 0xB9, 0x3D, 0x3C, 0xDC + }, + { + 0x09, 0x50, 0x6D, 0x93, 0x2D, 0xD1, 0x57, + 0xF6, 0x89, 0x8E, 0x7F, 0x7C, 0x16, 0xDE, + 0xC7, 0x7D, 0x64, 0x3B, 0x53, 0xF1, 0xA2, + 0xE5, 0x84, 0x0A, 0x76, 0x75, 0x01, 0xB4, + 0x88, 0x97, 0x0E, 0xA5 + }, + { + 0xE5, 0x6E, 0x8D, 0x10, 0x44, 0xF0, 0x8F, + 0xF0, 0x76, 0x8B, 0x3F, 0xE6, 0xEB, 0x4C, + 0x18, 0xF3, 0x91, 0x1A, 0x39, 0x65, 0xA8, + 0x0B, 0xBB, 0x89, 0x1B, 0x22, 0xB1, 0x8E, + 0x6D, 0xD6, 0xBE, 0xBC + }, + { + 0xD6, 0x00, 0x35, 0xFC, 0xDC, 0xDF, 0xEF, + 0xF2, 0x04, 0x36, 0x5C, 0xD8, 0x0C, 0x1D, + 0x9A, 0xAD, 0xAA, 0x28, 0x09, 0xEF, 0x40, + 0xD3, 0xAD, 0xBA, 0xF0, 0x01, 0xDB, 0x84, + 0xC2, 0x3C, 0xFF, 0x68 + }, + { + 0xA6, 0x1A, 0x12, 0x60, 0xB0, 0x52, 0x44, + 0x77, 0x32, 0xDD, 0x49, 0x8B, 0xAB, 0x19, + 0xD1, 0x8C, 0x28, 0x57, 0x2A, 0x97, 0xC5, + 0x76, 0xAA, 0x4B, 0x8D, 0xCB, 0x5C, 0xB4, + 0x54, 0xFB, 0x00, 0xCE + }, + { + 0xC0, 0x36, 0xFA, 0x72, 0xE5, 0x0C, 0x0C, + 0x03, 0x6F, 0xFA, 0x0D, 0x7C, 0xC4, 0x37, + 0xEE, 0x97, 0xDD, 0x6F, 0x03, 0x4C, 0xB3, + 0x10, 0x5D, 0x56, 0x85, 0x42, 0x4E, 0x84, + 0x71, 0x53, 0x63, 0x7A + }, + { + 0xA5, 0xDB, 0xCB, 0x43, 0x18, 0xE6, 0x9C, + 0x2C, 0x5F, 0x0D, 0x2D, 0x04, 0x83, 0xBF, + 0x7A, 0xB9, 0x85, 0x50, 0x78, 0x68, 0xFE, + 0xE0, 0xA4, 0x68, 0x53, 0xFD, 0xC7, 0xEE, + 0x00, 0xD2, 0x90, 0xB2 + }, + { + 0xD5, 0xD0, 0xC8, 0x9C, 0xCA, 0x98, 0x7D, + 0x74, 0xA2, 0xDC, 0xDD, 0xE6, 0xB8, 0x51, + 0x54, 0x87, 0xC1, 0xAA, 0x8A, 0x91, 0x67, + 0xA9, 0xAD, 0xFB, 0x4C, 0x65, 0x10, 0x33, + 0xAE, 0x1E, 0xF6, 0x4B + }, + { + 0x0D, 0x19, 0xBF, 0x36, 0x2A, 0x3D, 0x2E, + 0x81, 0x4A, 0x6A, 0xF2, 0x67, 0xC1, 0x03, + 0xB3, 0xF3, 0x34, 0x34, 0xE5, 0x55, 0xB5, + 0xA4, 0x6D, 0x47, 0xD9, 0x36, 0x8E, 0xEB, + 0x4C, 0xFA, 0xB8, 0x1E + }, + { + 0xCF, 0x65, 0x61, 0x9E, 0x11, 0x6B, 0x99, + 0x77, 0x16, 0xD6, 0xC3, 0x50, 0x40, 0x40, + 0x13, 0xE2, 0xA2, 0x23, 0x53, 0xCF, 0x84, + 0x5A, 0xC1, 0x0E, 0xAC, 0xFB, 0x10, 0xED, + 0x13, 0x29, 0xB6, 0x91 + }, + { + 0x2C, 0x33, 0x3C, 0x8D, 0x84, 0x6A, 0x45, + 0x15, 0xC1, 0x86, 0x23, 0xDF, 0xD9, 0x1B, + 0xA8, 0xD7, 0x9A, 0x62, 0x82, 0xED, 0xCE, + 0x54, 0x50, 0xFE, 0xCB, 0x9A, 0xB4, 0x5A, + 0x06, 0x7C, 0xF0, 0xB1 + }, + { + 0xE3, 0x54, 0xD8, 0xA9, 0xFD, 0x74, 0x1E, + 0x2C, 0x3E, 0xC4, 0x20, 0x31, 0xE8, 0xD8, + 0xDD, 0xE5, 0xCF, 0x4E, 0xCA, 0x66, 0xEA, + 0x88, 0xB6, 0x92, 0xA7, 0xE0, 0xA9, 0x03, + 0x88, 0x5B, 0x46, 0x70 + }, + { + 0x06, 0x4F, 0x84, 0xCE, 0x6C, 0x81, 0x65, + 0xF4, 0x5E, 0x89, 0x92, 0xA6, 0x64, 0xFA, + 0x4B, 0xE4, 0xAC, 0xFF, 0x2B, 0x59, 0x10, + 0x12, 0x7E, 0x8A, 0x4C, 0xD6, 0x79, 0x8D, + 0xE6, 0xDC, 0x0C, 0x23 + }, + { + 0x3D, 0xBD, 0x3E, 0x7A, 0x2A, 0x5C, 0xB5, + 0xC6, 0x99, 0xC7, 0xF1, 0xA0, 0xA9, 0x7A, + 0x0A, 0xE7, 0x64, 0xE9, 0xFE, 0xA9, 0x43, + 0xD5, 0x8B, 0xA0, 0x5B, 0xEF, 0x28, 0x2D, + 0x50, 0x4D, 0x21, 0x36 + }, + { + 0xCD, 0xD4, 0x0C, 0x1A, 0x19, 0x3E, 0xA0, + 0x4C, 0x67, 0x1A, 0xF6, 0xB7, 0x19, 0xC7, + 0x67, 0x45, 0xDC, 0x4D, 0xD8, 0x8F, 0xBE, + 0x8D, 0x7D, 0x2F, 0x9F, 0x1C, 0x7A, 0x68, + 0x7C, 0xBE, 0x58, 0x09 + }, + { + 0xA3, 0xB2, 0x59, 0x0E, 0xEC, 0x70, 0xA1, + 0xE3, 0x50, 0x3C, 0xB7, 0x40, 0x79, 0x54, + 0x0A, 0xD2, 0x43, 0x7A, 0xE7, 0xF6, 0x1C, + 0xDA, 0x42, 0x3E, 0x86, 0xF5, 0x7D, 0x3E, + 0x8E, 0x28, 0x14, 0x71 + }, + { + 0xA4, 0xF0, 0x81, 0xC0, 0xFE, 0xE6, 0xAB, + 0x5C, 0x05, 0xDC, 0xA9, 0x6A, 0x82, 0xCA, + 0x58, 0x09, 0x96, 0xAC, 0x6F, 0x7A, 0x1D, + 0xB6, 0xB9, 0x5A, 0x31, 0x35, 0x6B, 0x2B, + 0x87, 0x9C, 0x27, 0xB3 + }, + { + 0x12, 0x4D, 0xD9, 0xB3, 0xDC, 0x96, 0x8C, + 0x59, 0xB5, 0xCE, 0x2F, 0x99, 0x3D, 0xFA, + 0x38, 0xE0, 0x30, 0xAB, 0x4D, 0x65, 0xDA, + 0xC2, 0xFB, 0x0D, 0xFD, 0x9A, 0x61, 0xAD, + 0x09, 0xEC, 0x50, 0x21 + }, + { + 0xF1, 0xD8, 0x97, 0xC9, 0x72, 0x67, 0x7D, + 0x95, 0x67, 0x18, 0x11, 0x7B, 0xFA, 0x06, + 0x4C, 0x09, 0x74, 0x47, 0x46, 0x7A, 0x69, + 0x63, 0x9A, 0x8B, 0x8D, 0xD5, 0x88, 0x74, + 0xB5, 0xEC, 0x4D, 0x1D + }, + { + 0x31, 0xEF, 0xC0, 0x0E, 0x84, 0x65, 0xA4, + 0x82, 0x49, 0xE6, 0xB0, 0xA2, 0xDE, 0x7E, + 0xBE, 0x92, 0x8F, 0x2E, 0x7C, 0xED, 0x39, + 0x43, 0x05, 0xC0, 0x12, 0x44, 0x61, 0x94, + 0xD9, 0x5B, 0xFC, 0x43 + }, + { + 0xA2, 0xAC, 0x37, 0xCC, 0x80, 0xD4, 0x5B, + 0xB4, 0xFB, 0xE5, 0xDA, 0x1B, 0x05, 0xE6, + 0x86, 0xAF, 0x01, 0x79, 0xAF, 0x1A, 0xC2, + 0xA9, 0x0D, 0xB9, 0x82, 0xA3, 0x02, 0x04, + 0x6B, 0x0C, 0x2A, 0xA3 + }, + { + 0xE2, 0x30, 0x6B, 0xEA, 0x41, 0x92, 0x58, + 0xDF, 0x30, 0xD9, 0x06, 0x15, 0x2F, 0x97, + 0xDE, 0x36, 0x2A, 0x65, 0x1E, 0x7A, 0xF3, + 0x20, 0xFE, 0x2B, 0xA2, 0xFC, 0xF0, 0xBB, + 0xA2, 0x9E, 0x5A, 0x08 + }, + { + 0x30, 0xE7, 0xDA, 0x1A, 0xE0, 0xF7, 0x40, + 0x2C, 0x98, 0x39, 0x53, 0x77, 0x42, 0xA3, + 0xF3, 0x43, 0xF7, 0xA6, 0x3C, 0x4C, 0xBB, + 0x20, 0xB4, 0x19, 0x56, 0x5B, 0xB5, 0x6E, + 0xB2, 0x2E, 0x3F, 0x65 + }, + { + 0xC4, 0xC8, 0xF2, 0x15, 0x57, 0x53, 0xB5, + 0x6A, 0xFC, 0xC6, 0xFA, 0x44, 0xDE, 0x1F, + 0xB3, 0x93, 0x05, 0x10, 0x64, 0x21, 0x82, + 0xDF, 0x31, 0x3F, 0x6B, 0x94, 0xF4, 0xAD, + 0x43, 0xB1, 0xB1, 0x84 + }, + { + 0x32, 0xCF, 0x7A, 0x28, 0xB4, 0x9F, 0xF7, + 0xE7, 0x95, 0xB2, 0x7F, 0x42, 0xF4, 0x18, + 0x8E, 0x2A, 0xE6, 0x30, 0xEA, 0x1B, 0xDB, + 0x48, 0xF5, 0x48, 0x4E, 0x87, 0x7D, 0x19, + 0xB9, 0x6C, 0xBC, 0x4D + }, + { + 0x75, 0x81, 0x8A, 0x7A, 0xFE, 0x73, 0x62, + 0x01, 0xE3, 0x9C, 0xCA, 0x3B, 0xAD, 0xA1, + 0x7A, 0x71, 0xFE, 0x70, 0x62, 0x23, 0x54, + 0xAB, 0x31, 0x00, 0xA3, 0xE6, 0x8D, 0xC1, + 0x7C, 0xD2, 0x0B, 0x9A + }, + { + 0x0D, 0x4B, 0x95, 0x36, 0x8B, 0x1B, 0xB5, + 0xB3, 0x65, 0x22, 0xF1, 0x71, 0xA8, 0xE1, + 0xC1, 0xD4, 0xD8, 0xA7, 0x30, 0x3E, 0xB8, + 0xD3, 0x33, 0x98, 0x70, 0x1E, 0x4D, 0xD2, + 0x43, 0xC8, 0xC9, 0xEE + }, + { + 0x7C, 0x26, 0x7B, 0x34, 0x53, 0x94, 0xA1, + 0xB6, 0xA4, 0x70, 0xE4, 0x42, 0x58, 0x9B, + 0x9E, 0xF3, 0xD9, 0xB2, 0x19, 0x24, 0xD8, + 0xC2, 0x98, 0xCB, 0x27, 0x83, 0x72, 0x78, + 0x66, 0x7F, 0x1A, 0x98 + }, + { + 0xA0, 0xCA, 0x3B, 0x8D, 0x9B, 0x2C, 0x21, + 0x11, 0x3F, 0x73, 0x3B, 0x30, 0x6D, 0xA4, + 0x78, 0x57, 0xA4, 0xF9, 0xC8, 0xBB, 0xBE, + 0xB7, 0x7F, 0x54, 0x7C, 0x19, 0x77, 0xC5, + 0x45, 0xCF, 0xD5, 0x82 + }, + { + 0x7A, 0x5B, 0xA1, 0x22, 0xEE, 0xE9, 0x78, + 0x32, 0xB0, 0xA9, 0x82, 0x9F, 0x90, 0xCC, + 0xE4, 0xE8, 0x0E, 0x7E, 0x71, 0x2E, 0x84, + 0xE1, 0x50, 0x29, 0x24, 0x7F, 0x86, 0x62, + 0xFA, 0x21, 0xA2, 0x14 + }, + { + 0xD6, 0xC7, 0xF6, 0x7E, 0xFF, 0xE2, 0xAA, + 0x30, 0x84, 0x63, 0x8C, 0x81, 0x4D, 0xFC, + 0x3F, 0x31, 0x28, 0xFA, 0xAC, 0x62, 0x90, + 0x5F, 0x8D, 0x55, 0x9B, 0x36, 0x44, 0xFD, + 0x25, 0xC8, 0x87, 0x07 + }, + { + 0xB4, 0x83, 0x5A, 0x26, 0xA7, 0x6A, 0xFC, + 0x11, 0x48, 0xC3, 0xCA, 0xB1, 0xDB, 0x1D, + 0xC1, 0x41, 0x2D, 0xA6, 0x2A, 0x5C, 0xBD, + 0x97, 0x64, 0x71, 0xA8, 0x13, 0x88, 0xDB, + 0x99, 0x6E, 0x04, 0x89 + }, + { + 0x8B, 0x60, 0x29, 0x9F, 0x69, 0xA4, 0xCC, + 0x83, 0x92, 0x1E, 0x32, 0x59, 0xB7, 0x03, + 0x98, 0x2F, 0x0A, 0x5E, 0xDD, 0x34, 0x28, + 0x0E, 0xF0, 0xD9, 0xC7, 0x98, 0x05, 0xB2, + 0x3A, 0xA9, 0xC2, 0x54 + }, + { + 0xFE, 0x62, 0xB1, 0xCF, 0x39, 0x78, 0x86, + 0xB3, 0xBF, 0x47, 0x18, 0xCF, 0xA1, 0xCA, + 0xB8, 0xF9, 0x4F, 0xB2, 0x6D, 0x5D, 0x51, + 0xC5, 0x46, 0xB3, 0x77, 0xD0, 0x20, 0x97, + 0x07, 0xB2, 0x25, 0x4C + }, + { + 0x7D, 0x8A, 0x2A, 0xEF, 0x50, 0x3B, 0x9A, + 0xCD, 0x30, 0xF1, 0x48, 0x35, 0x41, 0xB5, + 0x19, 0xC2, 0x5B, 0xDF, 0x67, 0x5E, 0x93, + 0x89, 0xBA, 0x99, 0xD3, 0x12, 0xEC, 0xFE, + 0x37, 0x88, 0x4E, 0x64 + }, + { + 0x5F, 0x9E, 0x88, 0x8B, 0x4F, 0xE2, 0xBC, + 0x06, 0xEC, 0x13, 0x2B, 0x1A, 0x3E, 0x31, + 0xC2, 0xEE, 0xC0, 0xA4, 0x40, 0x5F, 0xD3, + 0xAB, 0xA4, 0x7D, 0xBC, 0xD4, 0x61, 0xED, + 0x6D, 0xBA, 0x71, 0x85 + }, + { + 0x9F, 0xC2, 0xD4, 0x74, 0x65, 0x5A, 0xC8, + 0x59, 0x04, 0x33, 0x0A, 0x45, 0x63, 0x33, + 0x84, 0x36, 0xDF, 0x94, 0xE7, 0xE0, 0x24, + 0xF2, 0xC1, 0xFF, 0x68, 0x58, 0xD1, 0xB0, + 0x30, 0x8E, 0x31, 0x48 + }, + { + 0x73, 0x00, 0xF1, 0xED, 0x9E, 0xB6, 0x65, + 0xF2, 0x1A, 0xAB, 0xEE, 0x4D, 0x02, 0x16, + 0xE6, 0xB1, 0x01, 0x8C, 0x3C, 0xE5, 0xD8, + 0xC2, 0xFF, 0xD2, 0xF0, 0x34, 0x70, 0x2C, + 0xC0, 0xF4, 0x30, 0x4A + }, + { + 0xD8, 0x9E, 0x22, 0xDF, 0x4B, 0xE0, 0x6D, + 0x2D, 0x59, 0xFF, 0x43, 0xCC, 0x29, 0xF7, + 0xB8, 0xEC, 0x75, 0xA6, 0x18, 0x0B, 0xFD, + 0x50, 0x50, 0xF2, 0xB7, 0xE1, 0xC7, 0xFB, + 0xE2, 0xFF, 0x59, 0x1F + }, + { + 0xE0, 0xE9, 0x19, 0x3F, 0xB0, 0x26, 0xAA, + 0x2B, 0x2C, 0xA4, 0xE8, 0xBF, 0x92, 0xED, + 0x25, 0x4A, 0xB8, 0xE8, 0x4D, 0x3D, 0x44, + 0x31, 0x08, 0xD0, 0x57, 0xC7, 0x78, 0x62, + 0x3B, 0xAC, 0x88, 0x70 + }, + { + 0xDB, 0x6C, 0xA5, 0x1E, 0xD9, 0xB6, 0x51, + 0xF5, 0x5A, 0x4C, 0xB1, 0xD0, 0x24, 0x24, + 0xF1, 0x71, 0x72, 0x3C, 0x29, 0x7F, 0x7D, + 0x8A, 0x38, 0x22, 0x4C, 0xD3, 0xB5, 0x4C, + 0xA8, 0x62, 0xB0, 0x7D + }, + { + 0x5C, 0x0A, 0xA5, 0x9F, 0xE2, 0xF5, 0x5E, + 0x10, 0x63, 0xE8, 0xFE, 0x09, 0x21, 0x5B, + 0x57, 0x13, 0xD8, 0xE4, 0x15, 0xA8, 0x02, + 0x83, 0x29, 0x25, 0xB7, 0x46, 0x66, 0xA9, + 0xB5, 0xDF, 0xEF, 0x9C + }, + { + 0xCC, 0xEE, 0x10, 0x09, 0x7C, 0xEE, 0x96, + 0x38, 0x03, 0x55, 0x09, 0xA9, 0x7A, 0x62, + 0x2B, 0x1D, 0xC3, 0xBC, 0x5A, 0xAC, 0x0F, + 0xD9, 0x01, 0x16, 0xBD, 0x43, 0xB8, 0xC5, + 0xE7, 0x1F, 0x9C, 0x19 + }, + { + 0x1B, 0xFB, 0xDA, 0x11, 0x46, 0xFF, 0xD9, + 0x12, 0x8C, 0x08, 0xD1, 0x91, 0x86, 0x4C, + 0x1B, 0x5E, 0x83, 0x93, 0x87, 0x1B, 0x8B, + 0x19, 0xBA, 0x5E, 0x8E, 0x3C, 0x5C, 0xAE, + 0x0C, 0x06, 0x78, 0x9A + }, + { + 0xA2, 0x70, 0xAB, 0xDF, 0x8B, 0x0D, 0x10, + 0x3B, 0xE7, 0x68, 0xEC, 0x6A, 0xF0, 0xF9, + 0x3B, 0xBE, 0x5A, 0xC4, 0xEE, 0xB6, 0xB7, + 0x80, 0x7E, 0x99, 0x39, 0x81, 0x45, 0x7D, + 0x2C, 0x22, 0x5E, 0x12 + }, + { + 0x83, 0x55, 0x86, 0x36, 0x8F, 0x1E, 0xC1, + 0x28, 0x29, 0xA7, 0x8F, 0x51, 0xDA, 0x40, + 0x17, 0xD6, 0x87, 0xEC, 0x84, 0x29, 0xB1, + 0xBC, 0x27, 0x11, 0xBC, 0xAA, 0x27, 0xAC, + 0xB6, 0xCA, 0x38, 0x5A + }, + { + 0xB4, 0x3E, 0x11, 0xB8, 0xF1, 0x36, 0x9B, + 0x7E, 0xF0, 0xDF, 0xFF, 0x62, 0x18, 0xDB, + 0xF4, 0xB8, 0x28, 0xB2, 0xBE, 0xD0, 0x91, + 0xE8, 0x5E, 0x58, 0xEE, 0xC4, 0xCD, 0xF3, + 0x68, 0x8B, 0xA5, 0x7E + }, + { + 0x1F, 0x2E, 0x07, 0x8A, 0x24, 0x6A, 0x8A, + 0x32, 0xCB, 0xAA, 0xCE, 0x6D, 0xD8, 0xC0, + 0x10, 0xA1, 0xF2, 0x78, 0x24, 0x69, 0x4E, + 0xA1, 0xC1, 0x5D, 0xC6, 0x08, 0x71, 0x77, + 0x51, 0xCF, 0xF2, 0x49 + }, + { + 0x9E, 0xA2, 0xD9, 0xC2, 0x91, 0xA9, 0x48, + 0x08, 0xC4, 0x88, 0x74, 0xE1, 0x6E, 0x10, + 0x7B, 0xBA, 0xE7, 0x45, 0xED, 0xE1, 0xAA, + 0x67, 0xBF, 0x63, 0xE9, 0x59, 0x32, 0xEE, + 0x98, 0x4D, 0xB2, 0xC1 + }, + { + 0x09, 0x65, 0x7A, 0x30, 0x96, 0x8D, 0x33, + 0xB6, 0x6B, 0x53, 0x07, 0xFC, 0x38, 0x33, + 0x86, 0x1B, 0xD1, 0x20, 0x38, 0x9B, 0x33, + 0x3B, 0x7C, 0x96, 0xD9, 0xF4, 0x79, 0x00, + 0x6C, 0xAC, 0xDD, 0x16 + }, + { + 0x94, 0x8E, 0x9E, 0xFF, 0x27, 0x27, 0x5C, + 0x73, 0x2E, 0x82, 0x1D, 0xE6, 0x9F, 0x03, + 0x4E, 0xA1, 0x7B, 0x72, 0x45, 0x21, 0x08, + 0x64, 0x9C, 0x2B, 0x42, 0x5A, 0x51, 0x2B, + 0xB2, 0xCE, 0x90, 0xFE + }, + { + 0xB3, 0xEE, 0xA0, 0xB5, 0x63, 0x26, 0x09, + 0x58, 0xA5, 0x6C, 0xC1, 0x39, 0xF7, 0xDF, + 0x52, 0x8E, 0xE8, 0xA7, 0x9A, 0x87, 0x63, + 0xA7, 0xAB, 0x04, 0x72, 0x81, 0xBA, 0x61, + 0x5E, 0xEC, 0x92, 0xD0 + }, + { + 0xF5, 0x84, 0x03, 0xBA, 0x73, 0xC2, 0xD0, + 0x70, 0xC9, 0xC5, 0x59, 0xD3, 0xA8, 0xFA, + 0xE6, 0x64, 0x57, 0xBC, 0x0C, 0xC8, 0xB3, + 0x46, 0xC5, 0xA6, 0xA1, 0x22, 0xFB, 0x57, + 0x7F, 0xCF, 0x54, 0x24 + }, + { + 0x95, 0xE7, 0x02, 0xF8, 0xB4, 0x96, 0xF4, + 0xB6, 0x54, 0x9C, 0xEE, 0x9E, 0x6A, 0x7C, + 0xB5, 0x9E, 0xB9, 0x35, 0x5C, 0xC1, 0x76, + 0x89, 0xFF, 0x51, 0xDB, 0xAB, 0xCD, 0x0D, + 0xCF, 0x76, 0x9C, 0x37 + }, + { + 0x9D, 0xEC, 0xE8, 0xD8, 0x42, 0xDA, 0x68, + 0xA8, 0x59, 0xA4, 0xCF, 0x57, 0x41, 0x03, + 0x6B, 0x7B, 0xF7, 0xA9, 0x4F, 0xD1, 0xE9, + 0x62, 0xAE, 0xE8, 0x9E, 0x02, 0x96, 0xA9, + 0x82, 0x8D, 0xF7, 0x27 + }, + }, + { /* group_pk */ + 0x02, 0x5B, 0x7B, 0xAA, 0x65, 0x29, 0xE6, 0x84, 0x50, + 0x75, 0x38, 0xB3, 0xF0, 0x66, 0xCD, 0xA2, 0x6D, 0x29, + 0xE3, 0x29, 0x5C, 0x30, 0x65, 0xCC, 0xE3, 0xC5, 0x85, + 0x5C, 0x0D, 0x89, 0x60, 0x78, 0x1D + }, + { /* msg */ + 0x01, 0x86, 0x08, 0x2D, 0xF9, 0x58, 0x00, 0xB6, 0xEB, + 0x4E, 0x8C, 0xA2, 0x69, 0x26, 0x28, 0xAF, 0x61, 0xA4, + 0xBA, 0xED, 0x46, 0xAA, 0x89, 0xEC, 0x4D, 0xA5, 0x64, + 0xBD, 0x03, 0xA3, 0x8B, 0xE7 + }, + { /* sid */ + 0xCF, 0x72, 0x10, 0x0C, 0xDC, 0xAA, 0x4D, 0xBF, 0x8E, + 0x30, 0x9E, 0x36, 0xF0, 0x4C, 0xBF, 0x48, 0xF6, 0x37, + 0x26, 0x9A, 0xA7, 0x6D, 0x52, 0xEE, 0x95, 0xBC, 0xC4, + 0x79, 0x00, 0x68, 0xA6, 0x66 + }, + { /* cosigner_pk */ + 0x02, 0x8C, 0x03, 0x5E, 0xB9, 0x76, 0x5E, 0x3E, 0xC7, + 0xF8, 0xE1, 0x63, 0x23, 0xB7, 0x0E, 0x68, 0xBB, 0xCF, + 0x7B, 0xFB, 0x19, 0xE2, 0xFB, 0x2B, 0x6B, 0x6F, 0x70, + 0x8B, 0x52, 0xD4, 0xF3, 0xA7, 0x33 + }, + { /* cosigner_pubnonce */ + 0x02, 0x8A, 0xB3, 0x20, 0x85, 0x31, 0x24, 0xE7, 0x60, + 0x29, 0x8B, 0x22, 0x26, 0xBB, 0x7D, 0x6F, 0x6A, 0x68, + 0x20, 0x5A, 0xFC, 0xD5, 0xD3, 0xA3, 0xA5, 0xAA, 0x6D, + 0x1E, 0x8C, 0x17, 0xA8, 0x1F, 0x56, 0x03, 0x00, 0x88, + 0x1A, 0x74, 0x3A, 0xA2, 0xF4, 0x6D, 0x7B, 0x7D, 0xBA, + 0x5D, 0x7A, 0x95, 0xEE, 0x13, 0x62, 0x8D, 0x52, 0x3F, + 0xE0, 0x4F, 0xD4, 0xB4, 0xA5, 0x19, 0xC6, 0xFD, 0xCE, + 0x39, 0x86, 0x14 + }, + { /* group_aggnonce */ + 0x03, 0x6C, 0xAA, 0x6E, 0x46, 0x23, 0x06, 0x5E, 0xD6, + 0x3F, 0xF6, 0x76, 0x05, 0xA7, 0xFA, 0x0A, 0xC8, 0x81, + 0xFE, 0x70, 0x92, 0x5F, 0xA4, 0xFB, 0x9F, 0xE1, 0xEB, + 0x80, 0xE5, 0x0D, 0xEC, 0xD3, 0xB7, 0x03, 0x6C, 0x8A, + 0x33, 0x55, 0xF0, 0x79, 0x65, 0x85, 0xA6, 0x13, 0x1A, + 0xEC, 0x6A, 0x8D, 0x54, 0xF4, 0x14, 0x2D, 0x86, 0x3B, + 0x0D, 0xE1, 0x61, 0xF2, 0xA4, 0xAB, 0xCF, 0x3A, 0xFC, + 0xF3, 0x96, 0x6A + }, + { /* group_pubnonce */ + 0x03, 0x6C, 0xAA, 0x6E, 0x46, 0x23, 0x06, 0x5E, 0xD6, + 0x3F, 0xF6, 0x76, 0x05, 0xA7, 0xFA, 0x0A, 0xC8, 0x81, + 0xFE, 0x70, 0x92, 0x5F, 0xA4, 0xFB, 0x9F, 0xE1, 0xEB, + 0x80, 0xE5, 0x0D, 0xEC, 0xD3, 0xB7, 0x02, 0xC9, 0x10, + 0x88, 0x26, 0x67, 0x7C, 0xAF, 0x7D, 0xB2, 0xAE, 0xD8, + 0x3C, 0x9D, 0x11, 0xBF, 0xF7, 0x9E, 0x1A, 0xFB, 0x68, + 0xB2, 0x77, 0x64, 0x25, 0xBC, 0x79, 0x23, 0xD1, 0x9C, + 0xAF, 0x40, 0xAE + }, + { /* group_psig */ + 0x2C, 0x74, 0x3F, 0xCD, 0xAF, 0x2A, 0x5A, 0x42, 0xE1, + 0x24, 0x4C, 0xFA, 0x16, 0x4C, 0xCF, 0xF8, 0x05, 0x3F, + 0x84, 0xE1, 0x6B, 0xC0, 0xCD, 0x9F, 0x3C, 0xC8, 0x2A, + 0x4E, 0x29, 0xF0, 0x9C, 0x18 + }, + { /* pubshares, participant 1 first */ + { + 0x02, 0x10, 0x23, 0x37, 0x70, 0x46, 0x45, + 0xB9, 0x94, 0xAD, 0x8D, 0xDA, 0x61, 0x0F, + 0xD2, 0xED, 0x32, 0xC0, 0xB2, 0x63, 0xD6, + 0xB5, 0xBA, 0x91, 0x11, 0xC1, 0x66, 0xBD, + 0xF0, 0x24, 0x1B, 0x7B, 0xF3 + }, + { + 0x03, 0x77, 0x75, 0x78, 0x54, 0xEB, 0xD7, + 0xBF, 0x26, 0x66, 0xAC, 0x43, 0xFE, 0xE3, + 0xFC, 0x34, 0x1D, 0xAC, 0x78, 0x3B, 0x68, + 0xE9, 0xAA, 0xFA, 0x2F, 0xEE, 0x98, 0x6B, + 0x55, 0x7F, 0xE9, 0xAC, 0x38 + }, + { + 0x03, 0xB2, 0xCD, 0x73, 0xD1, 0x7E, 0x2C, + 0x29, 0x74, 0xCA, 0x53, 0xCF, 0x1D, 0x36, + 0x10, 0x50, 0x63, 0x14, 0xD9, 0xFE, 0x7C, + 0x49, 0xEA, 0x59, 0x22, 0xBF, 0x10, 0xBA, + 0xB1, 0x4E, 0x46, 0x02, 0xEE + }, + { + 0x03, 0x41, 0x17, 0x48, 0xD4, 0x44, 0xC0, + 0x0F, 0x83, 0x6E, 0x01, 0xFA, 0xCB, 0xF1, + 0xFB, 0x50, 0xEF, 0x15, 0x55, 0xC9, 0xE4, + 0x0A, 0x81, 0x8B, 0xCF, 0x62, 0x3E, 0x75, + 0xC4, 0xCC, 0x92, 0x3D, 0x0F + }, + { + 0x03, 0x46, 0xC7, 0xFF, 0xFC, 0xC9, 0x20, + 0x7D, 0x79, 0x1C, 0xCB, 0x6F, 0xE0, 0x2A, + 0xE0, 0xFE, 0xC0, 0xC5, 0x1D, 0x11, 0x6D, + 0x8D, 0xE1, 0xBE, 0x87, 0x86, 0xED, 0x65, + 0xF0, 0x27, 0x93, 0xAD, 0x71 + }, + { + 0x03, 0x4E, 0xBA, 0x73, 0x07, 0x79, 0xE2, + 0xE2, 0x65, 0x62, 0x2E, 0x29, 0xFF, 0xDE, + 0x5C, 0x72, 0xB1, 0xB6, 0x5F, 0x0C, 0xB1, + 0x96, 0x8E, 0x3A, 0xBA, 0x8F, 0x74, 0x2E, + 0xC5, 0x2D, 0x64, 0x5B, 0xBC + }, + { + 0x02, 0xF2, 0x5C, 0x2D, 0x3C, 0xF4, 0xDB, + 0x3B, 0x74, 0x1C, 0xD0, 0xFE, 0x48, 0xB7, + 0xBE, 0x9B, 0x90, 0x2B, 0x61, 0x54, 0x55, + 0xC6, 0x47, 0x1A, 0x8B, 0xF0, 0xF1, 0xF2, + 0x92, 0x6C, 0x09, 0x3F, 0xA6 + }, + { + 0x02, 0x53, 0x9F, 0xA6, 0x62, 0xFD, 0x83, + 0x76, 0x65, 0x7C, 0xD2, 0x08, 0x61, 0x11, + 0x4E, 0x16, 0x7C, 0xFD, 0x5A, 0xD9, 0x59, + 0x54, 0x60, 0xFF, 0x34, 0x7E, 0xA8, 0x1F, + 0xE2, 0xB1, 0x49, 0xBF, 0x76 + }, + { + 0x03, 0x66, 0x2E, 0xA6, 0x27, 0x62, 0x0B, + 0xFE, 0x2C, 0xE0, 0x1D, 0x34, 0xAC, 0x39, + 0xF0, 0xA8, 0x0D, 0x15, 0x3A, 0xCE, 0x0F, + 0x55, 0xFF, 0x8C, 0xE8, 0x4A, 0x7E, 0x18, + 0x50, 0xBD, 0xDF, 0xC1, 0x20 + }, + }, + { /* pubnonces, the mu participants of round one */ + { + 0x03, 0xE2, 0x17, 0xF2, 0x3D, 0x66, 0xCE, + 0xC6, 0x73, 0x56, 0x66, 0x2D, 0x1B, 0x1C, + 0x50, 0x89, 0x7A, 0xD9, 0xB6, 0x79, 0x47, + 0xEA, 0x9D, 0x37, 0x91, 0x1A, 0x1B, 0xB1, + 0xBA, 0x6B, 0x50, 0x3F, 0xDD, 0x03, 0x37, + 0x4B, 0x08, 0xC4, 0x1E, 0x7C, 0xB7, 0x96, + 0xAD, 0x68, 0x04, 0xC2, 0xD7, 0x20, 0x44, + 0x3E, 0xDF, 0x7F, 0x16, 0x8A, 0xBA, 0xE2, + 0x4F, 0xEE, 0xDC, 0x38, 0xF1, 0x00, 0x27, + 0x2A, 0xDB, 0xBF + }, + { + 0x02, 0x0A, 0x68, 0xF4, 0x2F, 0x07, 0x09, + 0x67, 0xA2, 0xB8, 0xBC, 0xEA, 0x9B, 0xF1, + 0x2B, 0xC3, 0xB3, 0xFC, 0xE4, 0x50, 0x8F, + 0x07, 0x5E, 0xDE, 0x8D, 0xE2, 0x94, 0x01, + 0xDD, 0xB9, 0xEE, 0x3F, 0x13, 0x03, 0xFF, + 0x28, 0xEB, 0xA4, 0xF0, 0x63, 0x79, 0xA0, + 0x89, 0xDE, 0x5C, 0xB2, 0x96, 0xEC, 0x98, + 0x9B, 0x45, 0x0B, 0x24, 0x33, 0x79, 0x0F, + 0xC6, 0x12, 0xDC, 0xBB, 0xD6, 0x62, 0x89, + 0x72, 0xA0, 0xA1 + }, + { + 0x03, 0x07, 0x61, 0x4D, 0x80, 0xF1, 0x60, + 0x90, 0xCF, 0x99, 0xDA, 0x0D, 0x02, 0x7A, + 0x16, 0x53, 0xDE, 0xB5, 0x37, 0x3B, 0xF1, + 0x73, 0x0B, 0x87, 0xA6, 0xFB, 0xEB, 0x46, + 0xA3, 0x47, 0x64, 0xDB, 0xFD, 0x03, 0xBE, + 0x62, 0x9B, 0xBD, 0xE8, 0x37, 0x0F, 0xAF, + 0x54, 0x6A, 0x96, 0x31, 0x7A, 0x94, 0x68, + 0xA6, 0x21, 0x07, 0xF3, 0x76, 0xC7, 0xAC, + 0x15, 0x42, 0xD3, 0x59, 0x83, 0x1C, 0x2A, + 0xB9, 0x89, 0x9F + }, + { + 0x02, 0x51, 0x95, 0x06, 0xC1, 0x85, 0x8E, + 0x45, 0x91, 0x94, 0x34, 0xDA, 0xEB, 0xEB, + 0x04, 0xC8, 0xB2, 0x7B, 0xB3, 0x3A, 0x0A, + 0x63, 0x9B, 0x68, 0xA5, 0x44, 0x5E, 0xD9, + 0x96, 0xC6, 0xAA, 0x1E, 0x17, 0x03, 0x14, + 0xE0, 0x82, 0x8E, 0xA7, 0x69, 0x90, 0xAC, + 0x21, 0x60, 0x1B, 0xA9, 0xEA, 0x1E, 0x47, + 0xEA, 0x7F, 0x43, 0x63, 0xF2, 0x7F, 0x85, + 0x69, 0x3E, 0x3C, 0x96, 0xB9, 0xB6, 0xB3, + 0xB6, 0x2F, 0x84 + }, + { + 0x03, 0x4F, 0xCF, 0xFE, 0x39, 0xC2, 0x83, + 0x05, 0xEE, 0xD0, 0xBC, 0x16, 0xD2, 0xBF, + 0x5F, 0x62, 0x9E, 0xE0, 0x34, 0xDD, 0xCC, + 0x2E, 0xEB, 0xCC, 0xD1, 0xD4, 0xDA, 0xBF, + 0x3D, 0x30, 0x12, 0x50, 0x68, 0x02, 0x87, + 0xFB, 0x9A, 0xE3, 0xB4, 0x48, 0x9C, 0x1F, + 0xC9, 0xC8, 0x01, 0xE0, 0xA3, 0x1E, 0x31, + 0x53, 0xC3, 0xAF, 0x94, 0x5A, 0xEE, 0x7E, + 0x3C, 0x79, 0x4E, 0xEF, 0x14, 0x50, 0x15, + 0xC4, 0x41, 0xDE + }, + { + 0x03, 0x83, 0x80, 0xC4, 0x79, 0x82, 0x6B, + 0x2F, 0xC1, 0x0C, 0x5E, 0x4D, 0xDF, 0x2F, + 0xD6, 0x30, 0xD5, 0x47, 0x8D, 0x5D, 0xA7, + 0x76, 0x4D, 0x4D, 0x3F, 0x83, 0xA9, 0x7B, + 0x2E, 0xF3, 0x8C, 0xF7, 0xED, 0x02, 0xBB, + 0xED, 0x86, 0x10, 0x43, 0xBE, 0xF3, 0x0D, + 0xA2, 0xD9, 0x88, 0x38, 0x1B, 0xDC, 0xC6, + 0x24, 0x51, 0x2C, 0xE4, 0x70, 0x58, 0x15, + 0xEA, 0xDE, 0xCF, 0xE4, 0xF8, 0xC9, 0x1C, + 0x4A, 0x78, 0x50 + }, + { + 0x02, 0x1B, 0xCA, 0xDA, 0x51, 0x97, 0xD5, + 0x27, 0x11, 0x2A, 0x85, 0xAB, 0xFD, 0xF7, + 0xDF, 0xA0, 0x14, 0x73, 0x8E, 0x06, 0xAE, + 0x71, 0x5A, 0x7C, 0x4C, 0x92, 0x93, 0x65, + 0x89, 0xEA, 0xC8, 0xE3, 0x44, 0x03, 0x89, + 0xED, 0xBE, 0x93, 0xFD, 0xF2, 0x56, 0xEA, + 0x89, 0xF8, 0x66, 0x0A, 0xBA, 0x93, 0xA2, + 0x25, 0x6B, 0xB7, 0x6A, 0xF5, 0x97, 0xB1, + 0xC8, 0x51, 0x7B, 0x4F, 0x3B, 0x07, 0x9F, + 0x23, 0xB8, 0xC5 + }, + { + 0x03, 0xA3, 0x85, 0xAE, 0x22, 0xF5, 0x7B, + 0xDA, 0x6F, 0x88, 0x98, 0x30, 0xED, 0x1B, + 0x54, 0x48, 0xEB, 0xB5, 0x76, 0x7D, 0x8B, + 0x92, 0xBE, 0x40, 0xE6, 0x4C, 0xE1, 0xE8, + 0x11, 0xCD, 0x66, 0xC9, 0x67, 0x03, 0x63, + 0x2A, 0xF2, 0x91, 0x4C, 0x48, 0x18, 0x0D, + 0x3D, 0xF0, 0xDC, 0x6D, 0xD4, 0x05, 0x4E, + 0x63, 0x65, 0xB9, 0x72, 0x0E, 0xBC, 0x55, + 0x1C, 0x3A, 0xF5, 0xE4, 0xEA, 0xA5, 0x1D, + 0xB4, 0x0E, 0x6B + }, + { + 0x02, 0x6B, 0xEE, 0x8E, 0xA3, 0xD9, 0xA5, + 0x8F, 0xBB, 0x22, 0x65, 0xF2, 0x07, 0x12, + 0x7B, 0x5B, 0x8D, 0x35, 0xB2, 0x3D, 0x94, + 0x45, 0xB1, 0x26, 0x12, 0x28, 0xA0, 0xB8, + 0x49, 0x1A, 0xB3, 0x20, 0x57, 0x02, 0x57, + 0xEA, 0xF4, 0x0B, 0x4C, 0x78, 0x2A, 0x1F, + 0x38, 0x93, 0x46, 0x9B, 0xAA, 0x04, 0xED, + 0x72, 0x29, 0x5A, 0xEE, 0xC3, 0x2F, 0x87, + 0xB7, 0x5E, 0xEC, 0x2E, 0x90, 0x5D, 0x63, + 0x6A, 0x87, 0x22 + }, + }, + { /* signature shares, the first t participants */ + { + 0xB3, 0x26, 0x3B, 0x72, 0x86, 0x74, 0xA7, + 0x5F, 0x7D, 0x63, 0xA3, 0x20, 0x22, 0x87, + 0xEE, 0xC4, 0xB4, 0xF2, 0xD7, 0xAD, 0x51, + 0x8D, 0xD0, 0x4B, 0xF4, 0x60, 0x31, 0x6A, + 0x5D, 0xA3, 0x18, 0xC5 + }, + { + 0x4C, 0x56, 0xFC, 0xAD, 0xD9, 0x45, 0xC0, + 0x39, 0xE3, 0x53, 0x50, 0x2D, 0xB5, 0x49, + 0x14, 0x92, 0x2D, 0x97, 0x20, 0x1F, 0xDC, + 0x60, 0x64, 0xB0, 0x52, 0xAA, 0xA7, 0x08, + 0xE2, 0x3F, 0xF7, 0x72 + }, + { + 0x83, 0xDE, 0x75, 0x87, 0x1A, 0xFC, 0xB5, + 0x94, 0xF0, 0x78, 0xE6, 0xFA, 0x3E, 0x8F, + 0x11, 0x3A, 0xEF, 0x2C, 0x27, 0x6D, 0x46, + 0x30, 0xBD, 0x6C, 0xC9, 0xFD, 0xFF, 0xF5, + 0x4F, 0xB9, 0x09, 0xF9 + }, + { + 0xEF, 0xE0, 0x50, 0x2B, 0x59, 0xA2, 0xD2, + 0xE9, 0xB7, 0xA1, 0x31, 0xA4, 0xCE, 0xF9, + 0x5A, 0x71, 0xFB, 0x36, 0x46, 0x10, 0x9F, + 0x28, 0xFC, 0xC0, 0xFC, 0x58, 0xE1, 0xFD, + 0xCB, 0x9E, 0x52, 0xF7 + }, + { + 0x30, 0xCB, 0xEE, 0xED, 0x3D, 0xEB, 0x9E, + 0x67, 0x80, 0xE0, 0x31, 0x94, 0x17, 0xC8, + 0x0B, 0xC1, 0x8F, 0x6D, 0x40, 0xB2, 0x7C, + 0xED, 0xD4, 0xC7, 0x7B, 0x34, 0x82, 0x7F, + 0xD9, 0x54, 0x47, 0x0D + }, + }, + }, +}; + diff --git a/src/modules/iceberg/vpss.h b/src/modules/iceberg/vpss.h new file mode 100644 index 00000000..bac34f57 --- /dev/null +++ b/src/modules/iceberg/vpss.h @@ -0,0 +1,67 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_VPSS_H +#define SECP256K1_MODULE_ICEBERG_VPSS_H + +#include "../../../include/secp256k1.h" +#include "../../group.h" +#include "../../scalar.h" + +/* Verifiable pseudorandom secret sharing: the part that runs in the group. + * + * No secret enters this file. Everything here operates on published commitments + * and public participant indices, so there is nothing to leak and every routine + * is free to be variable time. + * + * Two operations, both consequences of the fact that Lagrange interpolation + * uses nothing but addition and multiplication by known scalars, and therefore + * survives being carried into the group: + * + * verify - honest shares are evaluations of one degree t-1 polynomial, so + * interpolating the published commitments must yield zero for every + * coefficient above x^(t-1). A participant who publishes anything + * else raises the degree and is caught. + * + * combine - the same interpolation, evaluated at zero, gives the commitment + * to the shared secret. Any valid quorum produces the same point. */ + +/* 1 if the m commitments are consistent with a polynomial of degree at most + * t-1, 0 otherwise, with two edges the mathematics does not have. Below t this + * returns 0, although a set that small always lies on such a polynomial: there + * is nothing to test, and a degree check is the wrong place to fail open. At + * exactly t it returns 1 without testing anything, because t points fix the + * polynomial and leave no high coefficient over. Only m > t proves something. + * + * idx holds m distinct participant indices and points their commitments, which + * may include the point at infinity. Soundness needs m >= 2t-1, so that at least + * t of the points are honest and pin the true polynomial. Enforcing it is the + * caller's job; this routine checks the degree and nothing else. + * + * On the paths that reach the transcript the points are normalized in place, + * following the convention of the serialization helpers this shares with the + * musig module; the two early returns above leave them alone. */ +static int secp256k1_vpss_verify_var(const secp256k1_context *ctx, const unsigned char *idx, secp256k1_ge *points, size_t m, unsigned int t); + +/* The interpolated commitment polynomial, evaluated at `at`. + * + * At zero this is the group's aggregate, which is what aggregation wants. At a + * participant's own index it is what that participant's contribution must have + * been, which is how a signer checks that a set of contributions it was handed + * belongs to the session it thinks it is in, without having to be one of the + * contributors, since it may have been offline when they were produced. */ +static int secp256k1_vpss_eval_at_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m, unsigned int at); + +/* r <- sum_j lambda_j * points[j], the same interpolation evaluated at zero: + * the commitment to the shared secret. + * + * Returns 1. The int is the return of secp256k1_ecmult_multi_var underneath, + * which with the current implementation can only fail on a callback that fails, + * and the callback here cannot. Callers check it anyway, as the musig module + * does at the same call for the same reason: the day somebody hands that + * multiexponentiation a scratch space, it can fail. */ +static int secp256k1_vpss_combine_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m); + +#endif /* SECP256K1_MODULE_ICEBERG_VPSS_H */ diff --git a/src/modules/iceberg/vpss_impl.h b/src/modules/iceberg/vpss_impl.h new file mode 100644 index 00000000..1e3c87c3 --- /dev/null +++ b/src/modules/iceberg/vpss_impl.h @@ -0,0 +1,161 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_MODULE_ICEBERG_VPSS_IMPL_H +#define SECP256K1_MODULE_ICEBERG_VPSS_IMPL_H + +#include "../../../include/secp256k1_iceberg.h" + +#include "vpss.h" +#include "scalar_poly_impl.h" + +#include "../../ecmult.h" +#include "../../group.h" +#include "../../hash.h" +#include "../../scalar.h" +#include "../../util.h" + +/* Both operations here are a weighted sum of the same points, so they share one + * callback and differ only in how the weights were computed. */ +typedef struct { + const secp256k1_ge *points; + const secp256k1_scalar *weights; +} secp256k1_vpss_multi_data; + +static int secp256k1_vpss_multi_callback(secp256k1_scalar *sc, secp256k1_ge *pt, size_t idx, void *data) { + secp256k1_vpss_multi_data *ctx = (secp256k1_vpss_multi_data *)data; + *sc = ctx->weights[idx]; + *pt = ctx->points[idx]; + return 1; +} + +static int secp256k1_vpss_weighted_sum_var(const secp256k1_context *ctx, secp256k1_gej *r, const secp256k1_ge *points, const secp256k1_scalar *weights, size_t m) { + secp256k1_vpss_multi_data data; + data.points = points; + data.weights = weights; + /* No scratch space: the library does not allocate at runtime, and with at + * most ten points the simple path this falls back to is the right one + * anyway. This mirrors what musig's key aggregation does. + * + * Which also means this cannot return 0. Without a scratch space + * ecmult_multi_var takes the simple path, and that fails only on a callback + * that fails; ours cannot. Reaching a zero here would take a change to one + * of those two things, which is why the callers still test it. */ + return secp256k1_ecmult_multi_var(&ctx->error_callback, NULL, r, NULL, + secp256k1_vpss_multi_callback, &data, m); +} + +/* Initializes SHA256 with fixed midstate. This midstate was computed by applying + * SHA256 to SHA256("Iceberg/batchcoef")||SHA256("Iceberg/batchcoef"). */ +static void secp256k1_vpss_batchcoef_sha256_tagged(secp256k1_sha256 *sha) { + static const uint32_t midstate[8] = { + 0xdbf8f1f6ul, 0xc46235d4ul, 0xc3d5e6fdul, 0xaed98a69ul, + 0x739fc2e8ul, 0x686b55faul, 0xb3b06820ul, 0x7f3c361bul + }; + secp256k1_sha256_initialize_midstate(sha, 64, midstate); +} + +static int secp256k1_vpss_verify_var(const secp256k1_context *ctx, const unsigned char *idx, secp256k1_ge *points, size_t m, unsigned int t) { + secp256k1_scalar basis[SECP256K1_ICEBERG_MAX_PARTICIPANTS * SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_sha256 transcript; + secp256k1_gej sum; + unsigned char header[2]; + unsigned char buf[33]; + size_t i, j; + + VERIFY_CHECK(t >= 1 && m <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + + /* Below t the loop at the end runs zero times, and an empty sum is the + * identity, so this would report success on a set too small to constrain + * anything. No caller reaches it today; a degree check is the wrong place to + * fail open if one ever does. */ + if (m < t) { + return 0; + } + /* With m == t the interpolation is exactly determined and there is no + * high coefficient left to test. Nothing has been proved, but nothing has + * been violated either. */ + if (m == t) { + return 1; + } + + /* The naive check tests each high coefficient separately: for every + * i in [t, m), sum_j basis[j][i] * points[j] must be the identity. Testing + * a random linear combination of those equations instead collapses m-t + * multiexponentiations into one. If any coefficient is non-zero, the + * combination is the identity only if the weights happen to lie on a + * hyperplane, which a hash commits them away from. The transcript covers + * every input that defines the statement, so weights are fixed only after + * the prover has committed to the points. */ + secp256k1_vpss_batchcoef_sha256_tagged(&transcript); + header[0] = (unsigned char)t; + header[1] = (unsigned char)m; + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &transcript, header, sizeof(header)); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &transcript, idx, m); + for (j = 0; j < m; j++) { + secp256k1_musig_ge_serialize_ext(buf, &points[j]); + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &transcript, buf, sizeof(buf)); + } + + secp256k1_scalarpoly_lagrange_basis_var(basis, idx, m); + for (j = 0; j < m; j++) { + secp256k1_scalar_set_int(&weights[j], 0); + } + for (i = t; i < m; i++) { + secp256k1_sha256 fork = transcript; + secp256k1_scalar rho, term; + unsigned char which = (unsigned char)i; + unsigned char out[32]; + + secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &fork, &which, 1); + secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &fork, out); + secp256k1_scalar_set_b32(&rho, out, NULL); + + for (j = 0; j < m; j++) { + secp256k1_scalar_mul(&term, &rho, &basis[j * m + i]); + secp256k1_scalar_add(&weights[j], &weights[j], &term); + } + } + + if (!secp256k1_vpss_weighted_sum_var(ctx, &sum, points, weights, m)) { + return 0; + } + return secp256k1_gej_is_infinity(&sum); +} + +static int secp256k1_vpss_eval_at_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m, unsigned int at) { + secp256k1_scalar weights[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + secp256k1_scalar denominators[SECP256K1_ICEBERG_MAX_PARTICIPANTS]; + size_t j; + + VERIFY_CHECK(m >= 1 && m <= SECP256K1_ICEBERG_MAX_PARTICIPANTS); + + /* Fill the tail as well: only the first m entries are read, but GCC cannot + * see that and warns on the partially filled array. */ + for (j = 0; j < SECP256K1_ICEBERG_MAX_PARTICIPANTS; j++) { + secp256k1_scalar_set_int(&denominators[j], 1); + } + + /* One inversion for the whole set instead of one per weight, as in + * secp256k1_rss_lagrange_weights_var. Both partial_sign and + * partial_sig_verify come through here once per nonce sharing, so twice + * apiece. */ + for (j = 0; j < m; j++) { + secp256k1_scalarpoly_lagrange_parts_var(&weights[j], &denominators[j], + idx, m, idx[j], at); + } + secp256k1_scalarpoly_inverse_batch_var(denominators, denominators, m); + for (j = 0; j < m; j++) { + secp256k1_scalar_mul(&weights[j], &weights[j], &denominators[j]); + } + return secp256k1_vpss_weighted_sum_var(ctx, r, points, weights, m); +} + +static int secp256k1_vpss_combine_var(const secp256k1_context *ctx, secp256k1_gej *r, const unsigned char *idx, const secp256k1_ge *points, size_t m) { + return secp256k1_vpss_eval_at_var(ctx, r, idx, points, m, 0); +} + +#endif /* SECP256K1_MODULE_ICEBERG_VPSS_IMPL_H */ diff --git a/src/secp256k1.c b/src/secp256k1.c index 427c491d..d1b2f7c6 100644 --- a/src/secp256k1.c +++ b/src/secp256k1.c @@ -960,3 +960,7 @@ static int secp256k1_ge_parse_ext(secp256k1_ge* ge, const unsigned char *in33) { #ifdef ENABLE_MODULE_CHILLDKG # include "modules/chilldkg/main_impl.h" #endif + +#ifdef ENABLE_MODULE_ICEBERG +# include "modules/iceberg/main_impl.h" +#endif diff --git a/src/tests.c b/src/tests.c index c221cb1c..112c6f8d 100644 --- a/src/tests.c +++ b/src/tests.c @@ -7928,6 +7928,10 @@ static void run_ecdsa_wycheproof(void) { # include "modules/chilldkg/tests_impl.h" #endif +#ifdef ENABLE_MODULE_ICEBERG +# include "modules/iceberg/tests_impl.h" +#endif + static void run_secp256k1_memczero_test(void) { unsigned char buf1[6] = {1, 2, 3, 4, 5, 6}; unsigned char buf2[sizeof(buf1)]; @@ -8301,6 +8305,9 @@ static const struct tf_test_module registry_modules[] = { #endif #ifdef ENABLE_MODULE_CHILLDKG MAKE_TEST_MODULE(chilldkg), +#endif +#ifdef ENABLE_MODULE_ICEBERG + MAKE_TEST_MODULE(iceberg), #endif MAKE_TEST_MODULE(utils), };