src/modules/iceberg/vectors.h opens with regeneration instructions that
name ./tools/test_vectors_iceberg_generate.py, and that script was never
committed. The same header calls the file "the one thing in this
repository that cannot be rebuilt from what the repository contains",
which the missing generator made true twice over: neither the reference
nor the tool that reads it was reachable from a clone.
Recovered from the tree the Iceberg C is vendored in, at
sources/secp256k1-kmp/native/secp256k1 of the benchmark repository
(bitcoin-core/secp256k1 branch iceberg-module, commit 96201552, per that
repository's PINS.txt). Confirmed to be the generator that produced the
checked-in vectors before committing it:
- its HEADER template reproduces the header of vectors.h exactly,
including the ICEBERG_VECTOR_MAX_PARTICIPANTS 9 and
ICEBERG_VECTOR_MAX_SEEDS 126 it computes from the configuration list;
- its seed rule, sha256("iceberg test vectors|<label>|<rank>"),
reproduces the checked-in 2of3 seeds byte for byte;
- the whole of that tree's src/modules/iceberg/ matches this one byte
for byte except tests_impl.h, which differs only because the tests
here are ported to the unit_test.h test-module framework.
The script cannot be re-run from a clone alone, by design: it takes the
nkohen/Iceberg Python reference as its argument and that reference is
deliberately not vendored, since it is not ours and pinning a copy would
hide it drifting. Its docstring gives the clone and the pinned commit
(7b55ef6d), which is now the whole of what regenerating requires.
Also restore the executable bit on the frost and chilldkg generators.
Both carry a usage line telling the reader to run them directly, and
every other generator in tools/ is 0755.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
703 lines
24 KiB
Python
Executable File
703 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import json
|
|
import re
|
|
import textwrap
|
|
|
|
if len(sys.argv) < 2:
|
|
print(
|
|
"This script converts the BIP 445 FROST signing test vectors in a given directory to a C file that can be used in the test framework."
|
|
)
|
|
print("Usage: %s <dir>" % sys.argv[0])
|
|
sys.exit(1)
|
|
|
|
# The curve order; partial signatures >= ORDER fail frost_partial_sig_parse.
|
|
ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
|
|
skipped_cases = []
|
|
|
|
|
|
def hexstr_to_intarray(str):
|
|
# Always emitted inside brace initializers. C90 forbids empty initializer
|
|
# braces, so an empty byte string becomes "{ 0 }". The associated length
|
|
# field is 0, so the padding byte is never read.
|
|
return ", ".join([f"0x{b:02X}" for b in bytes.fromhex(str)]) or "0"
|
|
|
|
|
|
def indent(s, level=1):
|
|
return textwrap.indent(s, 4 * level * " ")
|
|
|
|
|
|
def byte_array(hex_str):
|
|
return "{ %s }" % hexstr_to_intarray(hex_str)
|
|
|
|
|
|
def init_byte_array_maybe(hex_str):
|
|
"""Emits 'has_flag, { bytes }' for a possibly-None byte string."""
|
|
return "%d, %s" % (
|
|
0 if hex_str is None else 1,
|
|
byte_array(hex_str) if hex_str is not None else "{ 0 }",
|
|
)
|
|
|
|
|
|
def init_var_bytes(hex_str, max_len):
|
|
"""Emits 'has_flag, len, { bytes }' for a possibly-None variable-length byte string."""
|
|
has = 0 if hex_str is None else 1
|
|
b = bytes.fromhex(hex_str) if hex_str is not None else b""
|
|
return "%d, %d, { %s }" % (has, len(b), hexstr_to_intarray(hex_str or ""))
|
|
|
|
|
|
def init_indices(array):
|
|
return "%d, { %s }" % (len(array), ", ".join(map(str, array)) if array else "0")
|
|
|
|
|
|
def init_indices_maybe(array):
|
|
"""Emits 'has_flag, { indices }' for a possibly-None index list."""
|
|
return "%d, { %s }" % (
|
|
0 if array is None else 1,
|
|
", ".join(map(str, array)) if array else "0",
|
|
)
|
|
|
|
|
|
def init_is_xonly(case, max_tweaks):
|
|
is_xonly = case.get("is_xonly", [])
|
|
if len(is_xonly) > 0:
|
|
return ", ".join("1" if x else "0" for x in is_xonly)
|
|
return "0"
|
|
|
|
|
|
def parse_sign_error(case):
|
|
"""Maps the reference error of a sign/tweak error case to a frost_vec_error
|
|
code (and a blame index into the case's index lists where applicable)."""
|
|
err = case["error"]
|
|
if err["type"] == "InvalidContributionError":
|
|
assert err["contrib"] == "aggnonce"
|
|
return "FROST_VEC_ERR_AGGNONCE_PARSE", -1
|
|
msg = err["message"]
|
|
if msg == "The signer's id is missing from the ids list.":
|
|
return "FROST_VEC_ERR_SIGN", -1
|
|
if msg == "The ids list contains duplicate elements.":
|
|
return "FROST_VEC_ERR_SESSION_INIT", -1
|
|
if msg == "The signer's pubshare is missing from the pubshares list.":
|
|
return "FROST_VEC_ERR_SIGN", -1
|
|
if msg.startswith("Invalid pubshare at index"):
|
|
return "FROST_VEC_ERR_PUBSHARE_PARSE", int(re.search(r"index (\d+)", msg).group(1))
|
|
if msg == "The threshold public key must not be the point at infinity.":
|
|
return "FROST_VEC_ERR_SESSION_INIT", -1
|
|
if msg.startswith("Invalid id at index"):
|
|
return "FROST_VEC_ERR_SESSION_INIT", -1
|
|
if msg == "The provided key material is incorrect: the public shares do not match the threshold public key.":
|
|
return "FROST_VEC_ERR_SESSION_INIT", -1
|
|
if msg in ("first secnonce value is out of range.", "second secnonce value is out of range."):
|
|
return "FROST_VEC_ERR_SECNONCE", -1
|
|
if msg == "The number of signers must be between t and n.":
|
|
return "FROST_VEC_ERR_SESSION_INIT", -1
|
|
if msg == "The signer's secret share value is out of range.":
|
|
return "FROST_VEC_ERR_SIGN", -1
|
|
if msg == "The tweak value is out of range.":
|
|
return "FROST_VEC_ERR_TWEAK_ADD", -1
|
|
if msg == "The result of tweaking cannot be infinity.":
|
|
return "FROST_VEC_ERR_TWEAK_ADD", -1
|
|
sys.exit("Unknown sign error: %s" % msg)
|
|
|
|
|
|
def parse_det_error(case):
|
|
"""Maps the reference error of a deterministic_sign error case to a
|
|
frost_vec_error code."""
|
|
err = case["error"]
|
|
if err["type"] == "InvalidContributionError":
|
|
assert err["contrib"] == "aggothernonce"
|
|
return "FROST_VEC_ERR_AGGOTHERNONCE", -1
|
|
msg = err["message"]
|
|
if msg == "The tweak value is out of range.":
|
|
return "FROST_VEC_ERR_TWEAK_ADD", -1
|
|
if msg.startswith("Invalid pubshare at index"):
|
|
return "FROST_VEC_ERR_PUBSHARE_PARSE", int(re.search(r"index (\d+)", msg).group(1))
|
|
if msg in (
|
|
"The signer's id is missing from the ids list.",
|
|
"The ids list contains duplicate elements.",
|
|
"The signer's pubshare is missing from the pubshares list.",
|
|
"The provided key material is incorrect: the public shares do not match the threshold public key.",
|
|
"The number of signers must be between t and n.",
|
|
"The signer's secret share value is out of range.",
|
|
) or msg.startswith("Invalid id at index"):
|
|
return "FROST_VEC_ERR_DET_SIGN", -1
|
|
sys.exit("Unknown det_sign error: %s" % msg)
|
|
|
|
|
|
def parse_verify_error(case):
|
|
err = case["error"]
|
|
if err["type"] == "InvalidContributionError":
|
|
assert err["contrib"] == "pubnonce"
|
|
return "FROST_VEC_ERR_PUBNONCE_PARSE", err["signer_index"]
|
|
msg = err["message"]
|
|
if msg.startswith("Invalid pubshare at index"):
|
|
return "FROST_VEC_ERR_PUBSHARE_PARSE", int(re.search(r"index (\d+)", msg).group(1))
|
|
sys.exit("Unknown verify error: %s" % err)
|
|
|
|
|
|
s = (
|
|
"""/**
|
|
* Automatically generated by %s.
|
|
*
|
|
* The test vectors are from the BIP 445 reference repository
|
|
* https://github.com/siv2r/bip-frost-signing, pinned at commit
|
|
* bb5396f90d45ba5a954cbfd1af80f1b45e02b229 (BIP v0.10.0). They are used by the
|
|
* tests in src/modules/frost/tests_impl.h. */
|
|
"""
|
|
% sys.argv[0]
|
|
)
|
|
|
|
s += """
|
|
enum frost_vec_error {
|
|
/* Valid case. */
|
|
FROST_VEC_ERR_NONE,
|
|
/* frost_session_init returns 0 (invalid session parameters). */
|
|
FROST_VEC_ERR_SESSION_INIT,
|
|
/* frost_sign returns 0. */
|
|
FROST_VEC_ERR_SIGN,
|
|
/* frost_sign triggers the illegal-argument callback when loading the
|
|
* injected (corrupt) secnonce. */
|
|
FROST_VEC_ERR_SECNONCE,
|
|
/* frost_aggnonce_parse returns 0. */
|
|
FROST_VEC_ERR_AGGNONCE_PARSE,
|
|
/* frost_pubnonce_parse of the blamed pubnonce returns 0. */
|
|
FROST_VEC_ERR_PUBNONCE_PARSE,
|
|
/* secp256k1_ec_pubkey_parse of the blamed pubshare returns 0. */
|
|
FROST_VEC_ERR_PUBSHARE_PARSE,
|
|
/* frost_partial_sig_parse of the blamed partial signature returns 0. */
|
|
FROST_VEC_ERR_PSIG_PARSE,
|
|
/* Applying the tweak chain to the tweak cache fails. */
|
|
FROST_VEC_ERR_TWEAK_ADD,
|
|
/* frost_partial_sig_agg returns 0 (wrong number of partial signatures). */
|
|
FROST_VEC_ERR_AGG_LEN,
|
|
/* frost_deterministic_sign returns 0. */
|
|
FROST_VEC_ERR_DET_SIGN,
|
|
/* The aggothernonce is rejected: frost_aggnonce_parse or
|
|
* frost_deterministic_sign returns 0. */
|
|
FROST_VEC_ERR_AGGOTHERNONCE
|
|
};
|
|
"""
|
|
|
|
# Load all vector files first (to compute the shared maximum sizes).
|
|
nonce_gen_data = json.load(open(sys.argv[1] + "/nonce_gen_vectors.json"))
|
|
nonce_agg_data = json.load(open(sys.argv[1] + "/nonce_agg_vectors.json"))
|
|
sign_verify_data = json.load(open(sys.argv[1] + "/sign_verify_vectors.json"))
|
|
tweak_data = json.load(open(sys.argv[1] + "/tweak_vectors.json"))
|
|
sig_agg_data = json.load(open(sys.argv[1] + "/sig_agg_vectors.json"))
|
|
det_sign_data = json.load(open(sys.argv[1] + "/det_sign_vectors.json"))
|
|
|
|
# Skip tweak error cases that the C API cannot express: the C API applies
|
|
# tweaks individually via frost_pubkey_{xonly,ec}_tweak_add, so a mismatch
|
|
# between the number of tweaks and tweak modes cannot occur, and tweaks always
|
|
# have a fixed length of 32 bytes.
|
|
tweak_error_tests = []
|
|
for group in tweak_data["test_groups"]:
|
|
for case in group["error_tests"]:
|
|
if case["error"]["message"] in (
|
|
"The tweaks and is_xonly arrays must have the same length.",
|
|
"The tweak must be a 32-byte array.",
|
|
):
|
|
skipped_cases.append(
|
|
"tweak_vectors error tc_id %d (%s): not expressible in the C API (tweaks are applied individually and have a fixed length of 32 bytes)"
|
|
% (case["tc_id"], case["error"]["message"])
|
|
)
|
|
continue
|
|
tweak_error_tests.append((tweak_data["test_groups"].index(group), case))
|
|
|
|
all_groups = (
|
|
sign_verify_data["test_groups"]
|
|
+ tweak_data["test_groups"]
|
|
+ sig_agg_data["test_groups"]
|
|
+ det_sign_data["test_groups"]
|
|
)
|
|
max_pubshares = max(len(g["pubshares"]) for g in all_groups)
|
|
max_secshares = max(len(g.get("secshares", [])) for g in all_groups)
|
|
max_pubnonces = max(len(g.get("pubnonces", [])) for g in all_groups)
|
|
max_secnonces = max(len(g.get("secnonces", [])) for g in all_groups)
|
|
max_tweaks = max(len(g.get("tweaks", [])) for g in all_groups)
|
|
# A group's tweak pool may contain a tweak that is not 32 bytes long (only
|
|
# referenced by error cases that the C API cannot express).
|
|
max_tweak_len = max(
|
|
len(bytes.fromhex(t)) for g in all_groups for t in g.get("tweaks", [])
|
|
)
|
|
|
|
sign_cases = [
|
|
c
|
|
for g in sign_verify_data["test_groups"]
|
|
for c in g["valid_tests"] + g["sign_error_tests"]
|
|
] + [c for g in tweak_data["test_groups"] for c in g["valid_tests"]] + [
|
|
c for _, c in tweak_error_tests
|
|
]
|
|
det_cases = [
|
|
c
|
|
for g in det_sign_data["test_groups"]
|
|
for c in g["valid_tests"] + g["error_tests"]
|
|
]
|
|
verify_cases = [
|
|
c
|
|
for g in sign_verify_data["test_groups"]
|
|
for c in g["verify_fail_tests"] + g["verify_error_tests"]
|
|
]
|
|
sig_agg_cases = [
|
|
c for g in sig_agg_data["test_groups"] for c in g["valid_tests"] + g["error_tests"]
|
|
]
|
|
|
|
max_ids = max(len(c["ids"]) for c in sign_cases + det_cases + verify_cases + sig_agg_cases)
|
|
max_msg = max(
|
|
len(bytes.fromhex(c["msg"]))
|
|
for c in sign_cases + det_cases + verify_cases + sig_agg_cases
|
|
)
|
|
max_case_tweaks = max(
|
|
[len(c.get("tweak_indices", [])) for c in sign_cases + sig_agg_cases]
|
|
+ [len(c["tweaks"]) for c in det_cases]
|
|
)
|
|
max_extra_in = max(
|
|
len(bytes.fromhex(c["extra_in"] or "")) for c in nonce_gen_data["valid_tests"]
|
|
)
|
|
max_ng_msg = max(
|
|
len(bytes.fromhex(c["msg"] or "")) for c in nonce_gen_data["valid_tests"]
|
|
)
|
|
max_psigs = max(len(c["psigs"]) for c in sig_agg_cases)
|
|
max_na_indices = max(
|
|
len(c["pubnonce_indices"])
|
|
for c in nonce_agg_data["valid_tests"] + nonce_agg_data["error_tests"]
|
|
)
|
|
|
|
s += """
|
|
enum {
|
|
FROST_VEC_MAX_PUBSHARES = %d,
|
|
FROST_VEC_MAX_SECSHARES = %d,
|
|
FROST_VEC_MAX_PUBNONCES = %d,
|
|
FROST_VEC_MAX_SECNONCES = %d,
|
|
FROST_VEC_MAX_GROUP_TWEAKS = %d,
|
|
FROST_VEC_MAX_TWEAK_LEN = %d,
|
|
FROST_VEC_MAX_SIGNERS = %d,
|
|
FROST_VEC_MAX_TWEAKS = %d,
|
|
FROST_VEC_MAX_MSGLEN = %d,
|
|
FROST_VEC_MAX_PSIGS = %d
|
|
};
|
|
""" % (
|
|
max_pubshares,
|
|
max_secshares,
|
|
max_pubnonces,
|
|
max_secnonces,
|
|
max_tweaks,
|
|
max_tweak_len,
|
|
max_ids,
|
|
max_case_tweaks,
|
|
max(max_msg, max_ng_msg, 1),
|
|
max(max_psigs, 1),
|
|
)
|
|
|
|
s += """
|
|
/* A test group's shared key material. Entry i of each array belongs to the
|
|
* participant with id i; entries at indices >= n are deliberately bad values
|
|
* that only error cases select. */
|
|
struct frost_vec_group {
|
|
uint32_t threshold;
|
|
uint32_t n_participants;
|
|
unsigned char thresh_pk[33];
|
|
size_t n_pubshares;
|
|
unsigned char pubshares[FROST_VEC_MAX_PUBSHARES][33];
|
|
size_t n_secshares;
|
|
unsigned char secshares[FROST_VEC_MAX_SECSHARES][32];
|
|
size_t n_pubnonces;
|
|
unsigned char pubnonces[FROST_VEC_MAX_PUBNONCES][66];
|
|
size_t n_secnonces;
|
|
unsigned char secnonces[FROST_VEC_MAX_SECNONCES][64];
|
|
size_t n_tweaks;
|
|
unsigned char tweaks[FROST_VEC_MAX_GROUP_TWEAKS][FROST_VEC_MAX_TWEAK_LEN];
|
|
};
|
|
"""
|
|
|
|
|
|
def init_group(group):
|
|
def counted(key):
|
|
values = group.get(key, [])
|
|
# C90 forbids empty initializer braces; an empty group emits "{ { 0 } }"
|
|
# and its count field is 0, so the padding entry is never read.
|
|
inner = " { 0 } "
|
|
if values:
|
|
inner = "\n" + indent(",\n".join(byte_array(x) for x in values), 1) + "\n"
|
|
return "%d, {%s}" % (len(values), inner)
|
|
|
|
return "{ %d, %d, %s, %s, %s, %s, %s, %s }," % (
|
|
group["t"],
|
|
group["n"],
|
|
byte_array(group["thresh_pk"]),
|
|
counted("pubshares"),
|
|
counted("secshares"),
|
|
counted("pubnonces"),
|
|
counted("secnonces"),
|
|
counted("tweaks"),
|
|
)
|
|
|
|
|
|
def init_groups(name, data):
|
|
out = "static const struct frost_vec_group frost_vec_%s_groups[%d] = {\n" % (
|
|
name,
|
|
len(data["test_groups"]),
|
|
)
|
|
for group in data["test_groups"]:
|
|
out += indent(init_group(group) + "\n")
|
|
out += "};\n"
|
|
return out
|
|
|
|
|
|
# nonce_gen vectors
|
|
num_cases = len(nonce_gen_data["valid_tests"])
|
|
s += """
|
|
struct frost_vec_nonce_gen_case {
|
|
unsigned char rand[32];
|
|
int has_secshare;
|
|
unsigned char secshare[32];
|
|
int has_pubshare;
|
|
unsigned char pubshare[33];
|
|
int has_thresh_pk;
|
|
unsigned char thresh_pk_xonly[32];
|
|
int has_msg;
|
|
size_t msg_len;
|
|
unsigned char msg[FROST_VEC_MAX_MSGLEN];
|
|
int has_extra_in;
|
|
size_t extra_in_len;
|
|
unsigned char extra_in[%d];
|
|
unsigned char expected_secnonce[64];
|
|
unsigned char expected_pubnonce[66];
|
|
};
|
|
""" % max(
|
|
max_extra_in, 1
|
|
)
|
|
|
|
s += "static const struct frost_vec_nonce_gen_case frost_vec_nonce_gen_cases[%d] = {\n" % num_cases
|
|
for case in nonce_gen_data["valid_tests"]:
|
|
s += indent(
|
|
"{ %s, %s, %s, %s, %s, %s, %s },\n"
|
|
% (
|
|
byte_array(case["rand"]),
|
|
init_byte_array_maybe(case["secshare"]),
|
|
init_byte_array_maybe(case["pubshare"]),
|
|
init_byte_array_maybe(case["thresh_pk_xonly"]),
|
|
init_var_bytes(case["msg"], max_ng_msg),
|
|
init_var_bytes(case["extra_in"], max_extra_in),
|
|
byte_array(case["expected"][0]) + ", " + byte_array(case["expected"][1]),
|
|
)
|
|
)
|
|
s += "};\n"
|
|
|
|
# nonce_agg vectors
|
|
num_pubnonces = len(nonce_agg_data["pubnonces"])
|
|
s += """
|
|
struct frost_vec_nonce_agg_case {
|
|
size_t n_pubnonces;
|
|
size_t pubnonce_indices[%d];
|
|
/* -1 for valid cases; otherwise the position (in pubnonce_indices) of the
|
|
* pubnonce that must fail frost_pubnonce_parse. */
|
|
int error_index;
|
|
unsigned char expected[66];
|
|
};
|
|
""" % max_na_indices
|
|
|
|
s += "static const unsigned char frost_vec_nonce_agg_pubnonces[%d][66] = {\n" % num_pubnonces
|
|
s += indent(",\n".join(byte_array(x) for x in nonce_agg_data["pubnonces"])) + "\n"
|
|
s += "};\n"
|
|
|
|
for name, cases in (("valid", nonce_agg_data["valid_tests"]), ("error", nonce_agg_data["error_tests"])):
|
|
s += "static const struct frost_vec_nonce_agg_case frost_vec_nonce_agg_%s_cases[%d] = {\n" % (name, len(cases))
|
|
for case in cases:
|
|
s += indent(
|
|
"{ %d, { %s }, %d, %s },\n"
|
|
% (
|
|
len(case["pubnonce_indices"]),
|
|
", ".join(map(str, case["pubnonce_indices"])),
|
|
case["error"]["signer_index"] if "error" in case else -1,
|
|
byte_array(case["expected"]) if "expected" in case else "{ 0 }",
|
|
)
|
|
)
|
|
s += "};\n"
|
|
|
|
# Shared sign case struct (used by sign_verify and tweak vectors)
|
|
s += """
|
|
struct frost_vec_sign_case {
|
|
size_t group;
|
|
uint32_t my_id;
|
|
size_t n_ids;
|
|
uint32_t ids[FROST_VEC_MAX_SIGNERS];
|
|
int has_pubshares;
|
|
size_t pubshare_indices[FROST_VEC_MAX_SIGNERS];
|
|
size_t secshare_index;
|
|
size_t secnonce_index;
|
|
unsigned char aggnonce[66];
|
|
size_t msg_len;
|
|
unsigned char msg[FROST_VEC_MAX_MSGLEN];
|
|
size_t n_tweaks;
|
|
size_t tweak_indices[FROST_VEC_MAX_TWEAKS];
|
|
int is_xonly[FROST_VEC_MAX_TWEAKS];
|
|
/* FROST_VEC_ERR_NONE for valid cases, otherwise an enum frost_vec_error value. */
|
|
int error;
|
|
/* For FROST_VEC_ERR_PUBSHARE_PARSE: position in pubshare_indices. */
|
|
int error_index;
|
|
unsigned char expected[32];
|
|
};
|
|
"""
|
|
|
|
|
|
def init_sign_case(group_idx, case, error):
|
|
if error:
|
|
error_code, error_index = parse_sign_error(case)
|
|
expected = "{ 0 }"
|
|
else:
|
|
error_code, error_index = "FROST_VEC_ERR_NONE", -1
|
|
expected = byte_array(case["expected"])
|
|
return "{ %d, %d, %s, %s, %d, %d, %s, %s, %s, %s, %s, %d, %s }," % (
|
|
group_idx,
|
|
case["my_id"],
|
|
init_indices(case["ids"]),
|
|
init_indices_maybe(case["pubshare_indices"]),
|
|
case["secshare_index"],
|
|
case["secnonce_index"],
|
|
byte_array(case["aggnonce"]),
|
|
"%d, { %s }" % (len(bytes.fromhex(case["msg"])), hexstr_to_intarray(case["msg"])),
|
|
init_indices(case.get("tweak_indices", [])),
|
|
"{ %s }" % init_is_xonly(case, max_case_tweaks),
|
|
error_code,
|
|
error_index,
|
|
expected,
|
|
)
|
|
|
|
|
|
# sign_verify vectors
|
|
s += init_groups("sign", sign_verify_data)
|
|
for name, key, is_error in (
|
|
("sign_valid", "valid_tests", False),
|
|
("sign_error", "sign_error_tests", True),
|
|
):
|
|
flat = []
|
|
for gi, g in enumerate(sign_verify_data["test_groups"]):
|
|
for c in (g["valid_tests"] if not is_error else g["sign_error_tests"]):
|
|
flat.append((gi, c))
|
|
s += "static const struct frost_vec_sign_case frost_vec_%s_cases[%d] = {\n" % (name, len(flat))
|
|
for gi, c in flat:
|
|
s += indent(init_sign_case(gi, c, is_error) + "\n")
|
|
s += "};\n"
|
|
|
|
s += """
|
|
struct frost_vec_verify_case {
|
|
size_t group;
|
|
unsigned char psig[32];
|
|
size_t n_ids;
|
|
uint32_t ids[FROST_VEC_MAX_SIGNERS];
|
|
size_t pubshare_indices[FROST_VEC_MAX_SIGNERS];
|
|
size_t pubnonce_indices[FROST_VEC_MAX_SIGNERS];
|
|
size_t signer_index;
|
|
size_t msg_len;
|
|
unsigned char msg[FROST_VEC_MAX_MSGLEN];
|
|
/* 0 if the psig is not a valid scalar (frost_partial_sig_parse fails). */
|
|
int psig_parses;
|
|
/* 0 for verify_fail cases; otherwise an enum frost_vec_error value. */
|
|
int error;
|
|
/* Position in pubnonce_indices/pubshare_indices of the blamed value. */
|
|
int error_index;
|
|
};
|
|
"""
|
|
|
|
|
|
def init_verify_case(group_idx, case, is_error):
|
|
psig = bytes.fromhex(case["psig"])
|
|
psig_parses = 1 if int.from_bytes(psig, "big") < ORDER else 0
|
|
if is_error:
|
|
error_code, error_index = parse_verify_error(case)
|
|
else:
|
|
error_code, error_index = "FROST_VEC_ERR_NONE", -1
|
|
return "{ %d, %s, %s, { %s }, { %s }, %d, %s, %d, %s, %d }," % (
|
|
group_idx,
|
|
byte_array(case["psig"]),
|
|
init_indices(case["ids"]),
|
|
", ".join(map(str, case["pubshare_indices"])),
|
|
", ".join(map(str, case["pubnonce_indices"])),
|
|
case["signer_index"],
|
|
"%d, { %s }" % (len(bytes.fromhex(case["msg"])), hexstr_to_intarray(case["msg"])),
|
|
psig_parses,
|
|
error_code,
|
|
error_index,
|
|
)
|
|
|
|
|
|
for name, key, is_error in (
|
|
("verify_fail", "verify_fail_tests", False),
|
|
("verify_error", "verify_error_tests", True),
|
|
):
|
|
flat = []
|
|
for gi, g in enumerate(sign_verify_data["test_groups"]):
|
|
for c in g[key]:
|
|
flat.append((gi, c))
|
|
s += "static const struct frost_vec_verify_case frost_vec_%s_cases[%d] = {\n" % (name, len(flat))
|
|
for gi, c in flat:
|
|
s += indent(init_verify_case(gi, c, is_error) + "\n")
|
|
s += "};\n"
|
|
|
|
# tweak vectors (reuse the sign case struct; tweaks are indexed into the group)
|
|
s += init_groups("tweak", tweak_data)
|
|
flat_valid = []
|
|
for gi, g in enumerate(tweak_data["test_groups"]):
|
|
for c in g["valid_tests"]:
|
|
flat_valid.append((gi, c))
|
|
s += "static const struct frost_vec_sign_case frost_vec_tweak_valid_cases[%d] = {\n" % len(flat_valid)
|
|
for gi, c in flat_valid:
|
|
s += indent(init_sign_case(gi, c, False) + "\n")
|
|
s += "};\n"
|
|
s += "static const struct frost_vec_sign_case frost_vec_tweak_error_cases[%d] = {\n" % len(tweak_error_tests)
|
|
for gi, c in tweak_error_tests:
|
|
s += indent(init_sign_case(gi, c, True) + "\n")
|
|
s += "};\n"
|
|
|
|
# sig_agg vectors
|
|
s += init_groups("sig_agg", sig_agg_data)
|
|
s += """
|
|
struct frost_vec_sig_agg_case {
|
|
size_t group;
|
|
size_t n_ids;
|
|
uint32_t ids[FROST_VEC_MAX_SIGNERS];
|
|
int has_pubshares;
|
|
size_t pubshare_indices[FROST_VEC_MAX_SIGNERS];
|
|
unsigned char aggnonce[66];
|
|
size_t n_tweaks;
|
|
size_t tweak_indices[FROST_VEC_MAX_TWEAKS];
|
|
int is_xonly[FROST_VEC_MAX_TWEAKS];
|
|
size_t n_psigs;
|
|
unsigned char psigs[FROST_VEC_MAX_PSIGS][32];
|
|
size_t msg_len;
|
|
unsigned char msg[FROST_VEC_MAX_MSGLEN];
|
|
/* FROST_VEC_ERR_NONE for valid cases, otherwise an enum frost_vec_error value. */
|
|
int error;
|
|
/* For FROST_VEC_ERR_PSIG_PARSE: position in psigs. */
|
|
int error_index;
|
|
unsigned char expected[64];
|
|
};
|
|
"""
|
|
|
|
|
|
def init_sig_agg_case(group_idx, case, is_error):
|
|
if is_error:
|
|
err = case["error"]
|
|
if err["type"] == "InvalidContributionError":
|
|
assert err["contrib"] == "psig"
|
|
error_code, error_index = "FROST_VEC_ERR_PSIG_PARSE", err["signer_index"]
|
|
elif err["message"] == "The psigs and ids lists must have the same length.":
|
|
error_code, error_index = "FROST_VEC_ERR_AGG_LEN", -1
|
|
else:
|
|
sys.exit("Unknown sig_agg error: %s" % err)
|
|
expected = "{ 0 }"
|
|
else:
|
|
error_code, error_index = "FROST_VEC_ERR_NONE", -1
|
|
expected = byte_array(case["expected"])
|
|
return "{ %d, %s, %s, %s, %s, { %s }, %s, %s, %s, %d, %s }," % (
|
|
group_idx,
|
|
init_indices(case["ids"]),
|
|
init_indices_maybe(case["pubshare_indices"]),
|
|
byte_array(case["aggnonce"]),
|
|
init_indices(case["tweak_indices"]),
|
|
init_is_xonly(case, max_case_tweaks),
|
|
"%d, %s" % (
|
|
len(case["psigs"]),
|
|
"{ %s }" % ", ".join(byte_array(p) for p in case["psigs"]) if case["psigs"] else "{ { 0 } }",
|
|
),
|
|
"%d, { %s }" % (len(bytes.fromhex(case["msg"])), hexstr_to_intarray(case["msg"])),
|
|
error_code,
|
|
error_index,
|
|
expected,
|
|
)
|
|
|
|
|
|
for name, key, is_error in (
|
|
("sig_agg_valid", "valid_tests", False),
|
|
("sig_agg_error", "error_tests", True),
|
|
):
|
|
flat = []
|
|
for gi, g in enumerate(sig_agg_data["test_groups"]):
|
|
for c in g[key]:
|
|
flat.append((gi, c))
|
|
s += "static const struct frost_vec_sig_agg_case frost_vec_%s_cases[%d] = {\n" % (name, len(flat))
|
|
for gi, c in flat:
|
|
s += indent(init_sig_agg_case(gi, c, is_error) + "\n")
|
|
s += "};\n"
|
|
|
|
# det_sign vectors
|
|
s += init_groups("det_sign", det_sign_data)
|
|
s += """
|
|
struct frost_vec_det_sign_case {
|
|
size_t group;
|
|
uint32_t my_id;
|
|
size_t n_ids;
|
|
uint32_t ids[FROST_VEC_MAX_SIGNERS];
|
|
int has_pubshares;
|
|
size_t pubshare_indices[FROST_VEC_MAX_SIGNERS];
|
|
size_t secshare_index;
|
|
int has_aggothernonce;
|
|
unsigned char aggothernonce[66];
|
|
int has_aux_rand;
|
|
unsigned char aux_rand[32];
|
|
size_t msg_len;
|
|
unsigned char msg[FROST_VEC_MAX_MSGLEN];
|
|
size_t n_tweaks;
|
|
unsigned char tweaks[FROST_VEC_MAX_TWEAKS][32];
|
|
int is_xonly[FROST_VEC_MAX_TWEAKS];
|
|
/* FROST_VEC_ERR_NONE for valid cases, otherwise an enum frost_vec_error value. */
|
|
int error;
|
|
/* For FROST_VEC_ERR_PUBSHARE_PARSE: position in pubshare_indices. */
|
|
int error_index;
|
|
unsigned char expected_pubnonce[66];
|
|
unsigned char expected_psig[32];
|
|
};
|
|
"""
|
|
|
|
|
|
def init_det_case(group_idx, case, is_error):
|
|
if is_error:
|
|
error_code, error_index = parse_det_error(case)
|
|
expected = "{ 0 }, { 0 }"
|
|
else:
|
|
error_code, error_index = "FROST_VEC_ERR_NONE", -1
|
|
expected = byte_array(case["expected"][0]) + ", " + byte_array(case["expected"][1])
|
|
return "{ %d, %d, %s, %s, %d, %s, %s, %s, %s, %s, %s, %d, %s }," % (
|
|
group_idx,
|
|
case["my_id"],
|
|
init_indices(case["ids"]),
|
|
init_indices_maybe(case["pubshare_indices"]),
|
|
case["secshare_index"],
|
|
init_byte_array_maybe(case["aggothernonce"]),
|
|
init_byte_array_maybe(case["aux_rand"]),
|
|
"%d, { %s }" % (len(bytes.fromhex(case["msg"])), hexstr_to_intarray(case["msg"])),
|
|
"%d, %s" % (
|
|
len(case["tweaks"]),
|
|
"{ %s }" % ", ".join(byte_array(t) for t in case["tweaks"]) if case["tweaks"] else "{ { 0 } }",
|
|
),
|
|
"{ %s }" % init_is_xonly(case, max_case_tweaks),
|
|
error_code,
|
|
error_index,
|
|
expected,
|
|
)
|
|
|
|
|
|
for name, key, is_error in (
|
|
("det_sign_valid", "valid_tests", False),
|
|
("det_sign_error", "error_tests", True),
|
|
):
|
|
flat = []
|
|
for gi, g in enumerate(det_sign_data["test_groups"]):
|
|
for c in g[key]:
|
|
flat.append((gi, c))
|
|
s += "static const struct frost_vec_det_sign_case frost_vec_%s_cases[%d] = {\n" % (name, len(flat))
|
|
for gi, c in flat:
|
|
s += indent(init_det_case(gi, c, is_error) + "\n")
|
|
s += "};\n"
|
|
|
|
if skipped_cases:
|
|
s += "\n/* Skipped test cases (not expressible in the C API):\n"
|
|
for case in skipped_cases:
|
|
s += " * - %s\n" % case
|
|
s += " */\n"
|
|
|
|
print(s)
|