diff --git a/half-aggregation.mediawiki b/half-aggregation.mediawiki index 5667181..af29ada 100644 --- a/half-aggregation.mediawiki +++ b/half-aggregation.mediawiki @@ -69,18 +69,17 @@ Moreover, they came up with an elegant approach to incremental aggregation that === Specification === -The specification is written in [https://github.com/hacspec/hacspec hacspec], a language for formal specifications and a subset of rust. -It can be found in the [[hacspec-halfagg/src/halfagg.rs|hacspec-halfagg directory]]. -Note that the specification depends the hacspec library and a [https://github.com/hacspec/hacspec/pull/244 hacspec implementation of BIP 340]. +The specification is written in Python and can be found in the [[py-halfagg/halfagg.py|py-halfagg directory]] +Note that the specification depends on the [https://github.com/secp256k1lab/secp256k1lab secp256k1lab library]. === Test Vectors === -Preliminary test vectors are provided in [[hacspec-halfagg/tests/tests.rs|tests.rs]]. -The specification can be executed with the test vectors by running cargo test in the [[hacspec-halfagg|hacspec-halfagg directory]] (cargo is the [https://doc.rust-lang.org/stable/cargo/ rust package manager]). +Test vectors are provided in [[py-halfagg/vectors/|vectors]] and are generated with a [[py-halfagg/gen_test_vectors.py|generator script]]. +The specification can be executed with the test vectors by running [[py-halfagg/run_test_vectors.py|the run_test_vectors script]]. === Pseudocode === -The following pseudocode is ''not'' a specification but is only intended to augment the actual hacspec [[#specification|specification]]. +The following pseudocode is ''not'' a specification but is only intended to augment the actual Python [[#specification|specification]]. ==== Notation ==== diff --git a/py-halfagg/gen_test_vectors.py b/py-halfagg/gen_test_vectors.py new file mode 100644 index 0000000..16be0ae --- /dev/null +++ b/py-halfagg/gen_test_vectors.py @@ -0,0 +1,648 @@ +#!/usr/bin/env python3 +""" +Generate test vectors for Schnorr signature half-aggregation. + +Usage: + python gen_test_vectors.py + +Outputs: + - vectors/test_vectors_aggregate.csv + - vectors/test_vectors_incaggregate.csv + - vectors/test_vectors_verify.csv +""" + +import csv +import os +from pathlib import Path +import sys +from typing import List, Tuple + +from secp256k1lab.secp256k1 import GE, FE +from secp256k1lab.util import bytes_from_int, int_from_bytes +from secp256k1lab.bip340 import pubkey_gen, schnorr_sign, schnorr_verify +from halfagg import Aggregate, IncAggregate, hashHalfAgg_randomizer + + +ROOT = Path(__file__).resolve().parent +VECTORS_DIR = ROOT / "vectors" +AGGREGATE_VECTORS = VECTORS_DIR / 'test_vectors_aggregate.csv' +INCAGGREGATE_VECTORS = VECTORS_DIR / 'test_vectors_incaggregate.csv' +VERIFY_VECTORS = VECTORS_DIR / 'test_vectors_verify.csv' + +n = GE.ORDER +p = FE.SIZE + + +def create_signature(index: int) -> Tuple[bytes, bytes, bytes]: + """Create a deterministic (pubkey, message, signature) triple.""" + sk = bytes([index + 1] * 32) + pk = pubkey_gen(sk) + msg = bytes([index + 2] * 32) + aux = bytes([index + 3] * 32) + sig = schnorr_sign(msg, sk, aux) + return pk, msg, sig + + +def format_list(items: List[bytes], sep: str = ";") -> str: + """Format a list of bytes as semicolon-separated hex strings.""" + if not items: + return "" + return sep.join(item.hex().upper() for item in items) + + +def gen_aggregate_vectors(f): + """Generate test vectors for Aggregate function.""" + writer = csv.writer(f) + writer.writerow(( + "index", + "pubkeys", + "messages", + "signatures", + "expected_result", + "expected_aggsig", + "comment" + )) + + idx = 0 + sigs = [create_signature(i) for i in range(5)] + + # Success: Empty aggregation + aggsig_empty = Aggregate([]) + writer.writerow(( + idx, "", "", "", + "TRUE", + aggsig_empty.hex().upper(), + "Empty signature list" + )) + idx += 1 + + # Success: Single signature + pk0, msg0, sig0 = sigs[0] + aggsig_1 = Aggregate([(pk0, msg0, sig0)]) + writer.writerow(( + idx, + pk0.hex().upper(), + msg0.hex().upper(), + sig0.hex().upper(), + "TRUE", + aggsig_1.hex().upper(), + "Single signature" + )) + idx += 1 + + # Success: Two signatures + pk1, msg1, sig1 = sigs[1] + pms_2 = [(pk0, msg0, sig0), (pk1, msg1, sig1)] + aggsig_2 = Aggregate(pms_2) + writer.writerow(( + idx, + format_list([pk0, pk1]), + format_list([msg0, msg1]), + format_list([sig0, sig1]), + "TRUE", + aggsig_2.hex().upper(), + "Two signatures" + )) + idx += 1 + + # Success: Three signatures + pk2, msg2, sig2 = sigs[2] + pms_3 = [(pk0, msg0, sig0), (pk1, msg1, sig1), (pk2, msg2, sig2)] + aggsig_3 = Aggregate(pms_3) + writer.writerow(( + idx, + format_list([pk0, pk1, pk2]), + format_list([msg0, msg1, msg2]), + format_list([sig0, sig1, sig2]), + "TRUE", + aggsig_3.hex().upper(), + "Three signatures" + )) + idx += 1 + + # Success: Five signatures + pms_5 = [sigs[i] for i in range(5)] + aggsig_5 = Aggregate(pms_5) + writer.writerow(( + idx, + format_list([s[0] for s in pms_5]), + format_list([s[1] for s in pms_5]), + format_list([s[2] for s in pms_5]), + "TRUE", + aggsig_5.hex().upper(), + "Five signatures" + )) + idx += 1 + + # Success: Strange aggregation - individual invalid sigs that are valid + # in aggregate + pk_a, msg_a, sig_a = sigs[0] + pk_b, msg_b, sig_b = sigs[1] + pms_valid = [(pk_a, msg_a, sig_a), (pk_b, msg_b, sig_b)] + aggsig_valid = Aggregate(pms_valid) + + pmr = [] + z = [] + for i in range(2): + pk, msg, sig = pms_valid[i] + pmr.append((pk, msg, sig[:32])) + z.append(hashHalfAgg_randomizer(pmr, i)) + + sagg = int_from_bytes(aggsig_valid[64:96]) + s1_new = 0x123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0 % n + s0_new = ((sagg - z[1] * s1_new) * pow(z[0], -1, n)) % n + + sig_a_invalid = sig_a[:32] + bytes_from_int(s0_new) + assert not schnorr_verify(msg_a, pk_a, sig_a_invalid), "sig_a_invalid should not verify" + sig_b_invalid = sig_b[:32] + bytes_from_int(s1_new) + assert not schnorr_verify(msg_b, pk_b, sig_b_invalid), "sig_b_invalid should not verify" + + pms_strange = [(pk_a, msg_a, sig_a_invalid), (pk_b, msg_b, sig_b_invalid)] + aggsig_strange = Aggregate(pms_strange) + + writer.writerow(( + idx, + format_list([pk_a, pk_b]), + format_list([msg_a, msg_b]), + format_list([sig_a_invalid, sig_b_invalid]), + "TRUE", + aggsig_strange.hex().upper(), + "Strange aggregation - invalid individual sigs, valid aggregate" + )) + idx += 1 + + # Failure: Signature with s = n + invalid_sig_n = sig0[0:32] + bytes_from_int(n) + writer.writerow(( + idx, + pk0.hex().upper(), + msg0.hex().upper(), + invalid_sig_n.hex().upper(), + "FALSE", + "", + "Signature s = n (at boundary)" + )) + idx += 1 + + # Failure: Signature with s > n + invalid_sig_gt_n = sig0[0:32] + bytes_from_int(n + 1000) + writer.writerow(( + idx, + pk0.hex().upper(), + msg0.hex().upper(), + invalid_sig_gt_n.hex().upper(), + "FALSE", + "", + "Signature s > n" + )) + idx += 1 + + # Failure: Signature with s = 2^256 - 1 + invalid_sig_max = sig0[0:32] + bytes([0xff] * 32) + writer.writerow(( + idx, + pk0.hex().upper(), + msg0.hex().upper(), + invalid_sig_max.hex().upper(), + "FALSE", + "", + "Signature s = 2^256 - 1" + )) + idx += 1 + + +def gen_incaggregate_vectors(f): + """Generate test vectors for IncAggregate function.""" + writer = csv.writer(f) + writer.writerow(( + "index", + "aggsig", + "pm_aggd_pubkeys", + "pm_aggd_messages", + "pms_pubkeys", + "pms_messages", + "pms_signatures", + "expected_result", + "expected_aggsig", + "comment" + )) + + idx = 0 + sigs = [create_signature(i) for i in range(4)] + pk0, msg0, sig0 = sigs[0] + pk1, msg1, sig1 = sigs[1] + pk2, msg2, sig2 = sigs[2] + + # Success: Increment empty aggregate with single signature + empty_aggsig = bytes([0] * 32) + result_0 = IncAggregate(empty_aggsig, [], [(pk0, msg0, sig0)]) + writer.writerow(( + idx, + empty_aggsig.hex().upper(), + "", "", + pk0.hex().upper(), + msg0.hex().upper(), + sig0.hex().upper(), + "TRUE", + result_0.hex().upper(), + "Add single signature to empty aggregate" + )) + idx += 1 + + # Success: Increment single-sig aggregate with another signature + aggsig_1 = Aggregate([(pk0, msg0, sig0)]) + result_1 = IncAggregate(aggsig_1, [(pk0, msg0)], [(pk1, msg1, sig1)]) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + pk1.hex().upper(), + msg1.hex().upper(), + sig1.hex().upper(), + "TRUE", + result_1.hex().upper(), + "Add second signature to single-sig aggregate" + )) + idx += 1 + + # Success: Add two signatures at once + result_2 = IncAggregate(aggsig_1, [(pk0, msg0)], [(pk1, msg1, sig1), (pk2, msg2, sig2)]) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + format_list([pk1, pk2]), + format_list([msg1, msg2]), + format_list([sig1, sig2]), + "TRUE", + result_2.hex().upper(), + "Add two signatures at once" + )) + idx += 1 + + # Success: Increment with empty new signatures (no change) + result_3 = IncAggregate(aggsig_1, [(pk0, msg0)], []) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "", "", "", + "TRUE", + result_3.hex().upper(), + "Add no new signatures (unchanged)" + )) + idx += 1 + + # Failure: Wrong aggsig length (too long) + wrong_len_aggsig = bytes([0] * 64) + writer.writerow(( + idx, + wrong_len_aggsig.hex().upper(), + "", "", + "", "", "", + "FALSE", + "", + "Aggregate signature wrong length" + )) + idx += 1 + + # Failure: Empty aggsig + writer.writerow(( + idx, + "", + "", "", + "", "", "", + "FALSE", + "", + "Aggregate signature empty" + )) + idx += 1 + + # Failure: Existing aggsig with s = n + invalid_aggsig_s = bytes_from_int(n) + writer.writerow(( + idx, + invalid_aggsig_s.hex().upper(), + "", "", + "", "", "", + "FALSE", + "", + "Existing aggregate has s = n" + )) + idx += 1 + + # Failure: New signature with s = n + invalid_new_sig = sig0[0:32] + bytes_from_int(n) + writer.writerow(( + idx, + empty_aggsig.hex().upper(), + "", "", + pk0.hex().upper(), + msg0.hex().upper(), + invalid_new_sig.hex().upper(), + "FALSE", + "", + "New signature has s = n" + )) + idx += 1 + + +def gen_verify_vectors(f): + """Generate test vectors for VerifyAggregate function.""" + writer = csv.writer(f) + writer.writerow(( + "index", + "aggsig", + "pubkeys", + "messages", + "expected_result", + "comment" + )) + + idx = 0 + sigs = [create_signature(i) for i in range(5)] + pk0, msg0, sig0 = sigs[0] + pk1, msg1, sig1 = sigs[1] + pk2, msg2, sig2 = sigs[2] + + # Success: Empty signature list + writer.writerow(( + idx, + "0000000000000000000000000000000000000000000000000000000000000000", + "", "", + "TRUE", + "Empty signature list" + )) + idx += 1 + + # Success: Single signature + writer.writerow(( + idx, + "B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F", + "1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F", + "0202020202020202020202020202020202020202020202020202020202020202", + "TRUE", + "Single signature" + )) + idx += 1 + + # Success: Two signatures + writer.writerow(( + idx, + "B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8A3AFBDB45A6A34BF7C8C00F1B6D7E7D375B54540F13716C87B62E51E2F4F22FFBF8913EC53226A34892D60252A7052614CA79AE939986828D81D2311957371AD", + "1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B", + "0202020202020202020202020202020202020202020202020202020202020202;0505050505050505050505050505050505050505050505050505050505050505", + "TRUE", + "Two signatures" + )) + idx += 1 + + # Success: Three signatures + aggsig_3 = Aggregate([(pk0, msg0, sig0), (pk1, msg1, sig1), (pk2, msg2, sig2)]) + writer.writerow(( + idx, + aggsig_3.hex().upper(), + format_list([pk0, pk1, pk2]), + format_list([msg0, msg1, msg2]), + "TRUE", + "Three signatures" + )) + idx += 1 + + # Success: Five signatures + pms_5 = [sigs[i] for i in range(5)] + aggsig_5 = Aggregate(pms_5) + writer.writerow(( + idx, + aggsig_5.hex().upper(), + format_list([s[0] for s in pms_5]), + format_list([s[1] for s in pms_5]), + "TRUE", + "Five signatures" + )) + idx += 1 + + # Failure: Public key not on curve + aggsig_1 = Aggregate([(pk0, msg0, sig0)]) + invalid_x = 0x4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D # From BIP340 + invalid_pk = bytes_from_int(invalid_x) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + invalid_pk.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Public key not on curve" + )) + idx += 1 + + # Failure: Public key is zero + zero_pk = bytes([0] * 32) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + zero_pk.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Public key is zero" + )) + idx += 1 + + # Failure: Public key >= field size + pk_too_large = bytes_from_int(p) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + pk_too_large.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Public key >= field size" + )) + idx += 1 + + # Failure: R value not on curve + invalid_r = bytes_from_int(invalid_x) + invalid_r_aggsig = invalid_r + sig0[32:64] + writer.writerow(( + idx, + invalid_r_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "R value not on curve" + )) + idx += 1 + + # Failure: R value is zero + zero_r_aggsig = bytes([0] * 32) + sig0[32:64] + writer.writerow(( + idx, + zero_r_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "R value is zero" + )) + idx += 1 + + # Failure: R value >= field size + r_too_large_aggsig = bytes_from_int(p + 100) + sig0[32:64] + writer.writerow(( + idx, + r_too_large_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "R value >= field size" + )) + idx += 1 + + # Failure: Aggregate s = n + s_n_aggsig = sig0[0:32] + bytes_from_int(n) + writer.writerow(( + idx, + s_n_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Aggregate s = n" + )) + idx += 1 + + # Failure: Aggregate s > n + s_gt_n_aggsig = sig0[0:32] + bytes_from_int(n + 1000) + writer.writerow(( + idx, + s_gt_n_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Aggregate s > n" + )) + idx += 1 + + # Failure: Aggregate s = 2^256 - 1 + max_s_aggsig = sig0[0:32] + bytes([0xff] * 32) + writer.writerow(( + idx, + max_s_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Aggregate s = 2^256 - 1" + )) + idx += 1 + + # Failure: Wrong message + wrong_msg = bytes([0xff] * 32) + writer.writerow(( + idx, + aggsig_1.hex().upper(), + pk0.hex().upper(), + wrong_msg.hex().upper(), + "FALSE", + "Wrong message" + )) + idx += 1 + + # Failure: Wrong public key + writer.writerow(( + idx, + aggsig_1.hex().upper(), + pk1.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Wrong public key" + )) + idx += 1 + + # Failure: Tampered signature + tampered_aggsig = aggsig_1[:-1] + bytes([(aggsig_1[-1] + 1) % 256]) + writer.writerow(( + idx, + tampered_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Tampered signature (bit flip)" + )) + idx += 1 + + # Failure: Swapped order + aggsig_2 = Aggregate([(pk0, msg0, sig0), (pk1, msg1, sig1)]) + writer.writerow(( + idx, + aggsig_2.hex().upper(), + format_list([pk1, pk0]), + format_list([msg1, msg0]), + "FALSE", + "Swapped order" + )) + idx += 1 + + # Failure: s = n - 1 (valid range, wrong signature) + s_n_minus_1_aggsig = sig0[0:32] + bytes_from_int(n - 1) + writer.writerow(( + idx, + s_n_minus_1_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "s = n-1 (valid range, wrong sig)" + )) + idx += 1 + + # Failure: s = 0 (valid range, wrong signature) + s_zero_aggsig = sig0[0:32] + bytes([0] * 32) + writer.writerow(( + idx, + s_zero_aggsig.hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "s = 0 (valid range, wrong sig)" + )) + idx += 1 + + # Failure: Aggsig too short + writer.writerow(( + idx, + sig0[0:31].hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Aggregate signature too short" + )) + idx += 1 + + # Failure: Aggsig too long + writer.writerow(( + idx, + (aggsig_1 + bytes([0] * 32)).hex().upper(), + pk0.hex().upper(), + msg0.hex().upper(), + "FALSE", + "Aggregate signature too long" + )) + idx += 1 + + +if __name__ == "__main__": + os.makedirs(VECTORS_DIR, exist_ok=True) + + print(f"Generating {AGGREGATE_VECTORS}") + with open(AGGREGATE_VECTORS, "w", newline='', encoding="utf-8") as f: + gen_aggregate_vectors(f) + + print(f"Generating {INCAGGREGATE_VECTORS}") + with open(INCAGGREGATE_VECTORS, "w", newline='', encoding="utf-8") as f: + gen_incaggregate_vectors(f) + + print(f"Generating {VERIFY_VECTORS}") + with open(VERIFY_VECTORS, "w", newline='', encoding="utf-8") as f: + gen_verify_vectors(f) + + print("Done.") diff --git a/py-halfagg/halfagg.py b/py-halfagg/halfagg.py new file mode 100644 index 0000000..5b7b262 --- /dev/null +++ b/py-halfagg/halfagg.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +Schnorr signature half-aggregation reference implementation + +WARNING: This implementation is for demonstration purposes only and _not_ to +be used in production environments. +""" + +from secp256k1lab.secp256k1 import ( + G, + GE, + Scalar, +) +from secp256k1lab.util import ( + bytes_from_int, + int_from_bytes, + tagged_hash, +) + + +# Group order +n = GE.ORDER + +def Aggregate(pms): + """ + Aggregates an array of triples (public key, message, signature) into a single aggregate signature. + + :param pms: An array of triples (public key, message, signature). + :return: The aggregate signature. + """ + + # Let aggsig = bytes(0) + aggsig = bytes([0] * 32) + + pm_aggd = [] + if not VerifyAggregate(aggsig, pm_aggd): + raise + + # Return IncAggregate(aggsig, pms0..u-1); fail if that fails. + return IncAggregate(aggsig, pm_aggd, pms) + + +# IncAggregate(aggsig, pm_aggd0..v-1, pms_to_agg0..u-1) +def IncAggregate(aggsig, pm_aggd, pms_to_agg): + """ + Incrementally aggregates an additional array of triples (public key, message, signature) + into an existing aggregate signature. + + :param aggsig: A byte array representing the aggregate signature. + :param pm_aggd: An array of tuples (public key, message). + :param pms_to_agg: An array of triples (public key, message, signature). + :return: The new aggregate signature. + """ + + # Fail if v + u ≥ 2^16 + v = len(pm_aggd) + u = len(pms_to_agg) + if v + u >= 2**16: + raise ValueError("v + u must be less than 2^16") + + # Fail if len(aggsig) ≠ 32 * (v + 1) + if len(aggsig) != 32 * (v + 1): + raise ValueError("Length of aggsig must be 32 * (v + 1)") + + r_values = [] + pmr_to_agg = [] + + # For i = 0 .. v-1: + for i in range(v): + # Let (pki, mi) = pm_aggdi + (pki, mi) = pm_aggd[i] + + # Let ri = aggsig[i⋅32:(i+1)⋅32] + ri = aggsig[i * 32:(i + 1) * 32] + r_values.append(ri) + + pmr_to_agg.append((pki, mi, ri)) + + z_values = [] + s_values = [] + + # For i = v .. v+u-1: + for i in range(v, v + u): + # Let (pki, mi, sigi) = pms_to_aggi-v + (pki, mi, sigi) = pms_to_agg[i - v] + + # Let ri = sigi[0:32] + ri = sigi[0:32] + r_values.append(ri) + + # Let si = int(sigi[32:64]); fail if si ≥ n + si = int_from_bytes(sigi[32:64]) + if si >= n: + raise ValueError("si must be less than n") + s_values.append(si) + + # If i = 0: + # Let zi = 1 + # Else: + # Let zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n + pmr_to_agg += [(pki, mi, sigi[0:32]) for (pki, mi, sigi) in pms_to_agg] + z_values.append(hashHalfAgg_randomizer(pmr_to_agg, i)) + + # Let s = int(aggsig[(v⋅32:(v+1)⋅32]) + zv⋅sv + ... + zv+u-1⋅sv+u-1 mod n + s = int_from_bytes(aggsig[v * 32:(v + 1) * 32]) + if s >= n: + raise ValueError("s must be less than n") + + for i in range(u): + s = (s + z_values[i] * s_values[i]) % n + + # Return r0 || ... || rv+u-1 || bytes(s) + return b''.join(r_values) + bytes_from_int(s) + + +# VerifyAggregate(aggsig, pm_aggd0..u-1) +def VerifyAggregate(aggsig, pm_aggd): + """ + Verifies an aggregate signature against an array of public key and message tuples. + + :param aggsig: A byte array representing the aggregate signature. + :param pm_aggd: An array of tuples (public key, message). + :return: Boolean indicating whether the verification is successful. + """ + + # Fail if u ≥ 216 + u = len(pm_aggd) + if u >= 2**16: + raise ValueError("u must be less than 2^16") + + # Fail if len(aggsig) ≠ 32 * (u + 1) + if len(aggsig) != 32 * (u + 1): + raise ValueError("Length of aggsig must be 32 * (u + 1)") + + z_values = [] + R_values = [] + P_values = [] + e_values = [] + r_values = [] + + # For i = 0 .. u-1: + for i in range(u): + # Let (pki, mi) = pm_aggdi + (pki, mi) = pm_aggd[i] + + # Let Pi = lift_x(int(pki)); fail if that fails + try: + Pi = GE.lift_x(int_from_bytes(pki)) + except ValueError: + return False + P_values.append(Pi) + + # Let ri = aggsig[i⋅32:(i+1)⋅32] + ri = aggsig[i * 32:(i + 1) * 32] + # Let Ri = lift_x(int(ri)); fail if that fails + try: + Ri = GE.lift_x(int_from_bytes(ri)) + except ValueError: + return False + R_values.append(Ri) + r_values.append(ri) + + # Let ei = int(hashBIP0340/challenge(bytes(ri) || pki || mi)) mod n + ei = int_from_bytes(hashBIP0340_challenge(ri, pki, mi)) % n + e_values.append(ei) + + # If i = 0: + # Let zi = 1 + # Else: + # Let zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n + pmr = [(pki, mi, ri) for (pki, mi), ri in zip(pm_aggd, r_values)] + z_values.append(hashHalfAgg_randomizer(pmr, i)) + + # Let s = int(aggsig[u⋅32:(u+1)⋅32]); fail if s ≥ n + s = int_from_bytes(aggsig[u * 32:(u + 1) * 32]) + if s >= n: + return False + s = Scalar.from_int_checked(s) + + # Fail if s⋅G ≠ z0⋅(R0 + e0⋅P0) + ... + zu-1⋅(Ru-1 + eu-1⋅Pu-1) + lhs = s * G + rhs = GE() + for i in range(u): + e = Scalar.from_int_checked(e_values[i]) + P = P_values[i] + R = R_values[i] + rhsi = R + e * P + + z = Scalar.from_int_checked(z_values[i]) + rhsi = z * rhsi + + rhs = rhs + rhsi + + return lhs == rhs + + +def hashBIP0340_challenge(sig, pubkey, msg): + return tagged_hash("BIP0340/challenge", sig + pubkey + msg) + + +def hashHalfAgg_randomizer(pmr, index): + if index == 0: + return 1 + + random_input = bytes() + for i in range(index + 1): + (pki, mi, ri) = pmr[i] + random_input += ri + random_input += pki + random_input += mi + + return int_from_bytes(tagged_hash("HalfAgg/randomizer", random_input)) % n diff --git a/py-halfagg/run_test_vectors.py b/py-halfagg/run_test_vectors.py new file mode 100644 index 0000000..9a588e6 --- /dev/null +++ b/py-halfagg/run_test_vectors.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Run test vectors for Schnorr signature half-aggregation. + +Usage: + python run_test_vectors.py + +Expects CSV files generated by gen_test_vectors.py in vectors/ directory. +""" + +import csv +import os +from pathlib import Path +import sys +from typing import List, Tuple + +from halfagg import Aggregate, IncAggregate, VerifyAggregate + + +ROOT = Path(__file__).resolve().parent +VECTORS_DIR = ROOT / "vectors" +AGGREGATE_VECTORS = VECTORS_DIR / 'test_vectors_aggregate.csv' +INCAGGREGATE_VECTORS = VECTORS_DIR / 'test_vectors_incaggregate.csv' +VERIFY_VECTORS = VECTORS_DIR / 'test_vectors_verify.csv' + + +def parse_hex_list(hex_str: str) -> List[bytes]: + if not hex_str: + return [] + return [bytes.fromhex(h) for h in hex_str.split(';')] + + +def parse_pms(pks_str: str, msgs_str: str, sigs_str: str) -> List[Tuple[bytes, bytes, bytes]]: + pks = parse_hex_list(pks_str) + msgs = parse_hex_list(msgs_str) + sigs = parse_hex_list(sigs_str) + if not pks: + return [] + return list(zip(pks, msgs, sigs)) + + +def parse_pm(pks_str: str, msgs_str: str) -> List[Tuple[bytes, bytes]]: + pks = parse_hex_list(pks_str) + msgs = parse_hex_list(msgs_str) + if not pks: + return [] + return list(zip(pks, msgs)) + + +def run_aggregate_tests() -> List[str]: + """Run Aggregate test vectors. Returns list of failed test descriptions.""" + failures = [] + + with open(AGGREGATE_VECTORS, newline='', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + index, pks_str, msgs_str, sigs_str, expected_result, expected_aggsig, comment = row + pms = parse_pms(pks_str, msgs_str, sigs_str) + + if expected_result == "FALSE": + try: + Aggregate(pms) + failures.append(f"Aggregate #{index}: {comment}") + except: + pass + else: + try: + actual = Aggregate(pms) + if actual != bytes.fromhex(expected_aggsig): + failures.append(f"Aggregate #{index}: {comment}") + except: + failures.append(f"Aggregate #{index}: {comment}") + + return failures + + +def run_incaggregate_tests() -> List[str]: + """Run IncAggregate test vectors. Returns list of failed test descriptions.""" + failures = [] + + with open(INCAGGREGATE_VECTORS, newline='', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + (index, aggsig_str, pm_pks_str, pm_msgs_str, + pms_pks_str, pms_msgs_str, pms_sigs_str, + expected_result, expected_aggsig, comment) = row + + aggsig = bytes.fromhex(aggsig_str) if aggsig_str else bytes() + pm_aggd = parse_pm(pm_pks_str, pm_msgs_str) + pms_to_agg = parse_pms(pms_pks_str, pms_msgs_str, pms_sigs_str) + + if expected_result == "FALSE": + try: + IncAggregate(aggsig, pm_aggd, pms_to_agg) + failures.append(f"IncAggregate #{index}: {comment}") + except: + pass + else: + try: + actual = IncAggregate(aggsig, pm_aggd, pms_to_agg) + if actual != bytes.fromhex(expected_aggsig): + failures.append(f"IncAggregate #{index}: {comment}") + except: + failures.append(f"IncAggregate #{index}: {comment}") + + return failures + + +def run_verify_tests() -> List[str]: + """Run VerifyAggregate test vectors. Returns list of failed test descriptions.""" + failures = [] + + with open(VERIFY_VECTORS, newline='', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + index, aggsig_str, pks_str, msgs_str, expected_result, comment = row + + aggsig = bytes.fromhex(aggsig_str) + pm = parse_pm(pks_str, msgs_str) + + try: + result = VerifyAggregate(aggsig, pm) + expected = (expected_result == "TRUE") + if result != expected: + failures.append(f"VerifyAggregate #{index}: {comment}") + except: + if expected_result == "TRUE": + failures.append(f"VerifyAggregate #{index}: {comment}") + + return failures + + +def run_count_limit_tests() -> List[str]: + """Run count limit tests. Returns list of failed test descriptions.""" + failures = [] + + big_pms = [(bytes([1] * 32), bytes([2] * 32), bytes([3] * 64)) for _ in range(0x10000)] + big_pm = [(bytes([1] * 32), bytes([2] * 32)) for _ in range(0x10000)] + + try: + Aggregate(big_pms) + failures.append("CountLimit: Aggregate with 2^16 signatures") + except: + pass + + try: + IncAggregate(bytes([0] * 32), big_pm, []) + failures.append("CountLimit: IncAggregate with 2^16 existing") + except: + pass + + try: + IncAggregate(bytes([0] * 32), [], big_pms) + failures.append("CountLimit: IncAggregate with 2^16 new") + except: + pass + + try: + VerifyAggregate(bytes([0] * 32), big_pm) + failures.append("CountLimit: VerifyAggregate with 2^16") + except: + pass + + return failures + + +def main(): + for filename in [AGGREGATE_VECTORS, INCAGGREGATE_VECTORS, VERIFY_VECTORS]: + if not os.path.exists(filename): + print(f"ERROR: Missing {filename}. Run gen_test_vectors.py first.") + sys.exit(1) + + failures = [] + failures.extend(run_aggregate_tests()) + failures.extend(run_incaggregate_tests()) + failures.extend(run_verify_tests()) + failures.extend(run_count_limit_tests()) + + if failures: + print("FAILED:") + for f in failures: + print(f" {f}") + sys.exit(1) + else: + print("All tests have passed.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/py-halfagg/vectors/test_vectors_aggregate.csv b/py-halfagg/vectors/test_vectors_aggregate.csv new file mode 100644 index 0000000..bdedae7 --- /dev/null +++ b/py-halfagg/vectors/test_vectors_aggregate.csv @@ -0,0 +1,10 @@ +index,pubkeys,messages,signatures,expected_result,expected_aggsig,comment +0,,,,TRUE,0000000000000000000000000000000000000000000000000000000000000000,Empty signature list +1,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,Single signature +2,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766,0202020202020202020202020202020202020202020202020202020202020202;0303030303030303030303030303030303030303030303030303030303030303,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F;C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9233428AEB77468852520B0256DE84562A29629194DAEAC45444C7711CD379E57,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9DF1D0EE03A7E5D274A857BF080C8BD3B5244BC1A9D31F660B8E22F309260FF26,Two signatures +3,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766;531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337,0202020202020202020202020202020202020202020202020202020202020202;0303030303030303030303030303030303030303030303030303030303030303;0404040404040404040404040404040404040404040404040404040404040404,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F;C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9233428AEB77468852520B0256DE84562A29629194DAEAC45444C7711CD379E57;F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F923A17EA0CD56ED3A824AA14BC67839CED465BA5269700F600EDFC3DAAC8A6B069,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F92F69F0DB595934BE97771DEF114962A9277861BFD5858D2E364DCCF30266F8CD4,Three signatures +4,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766;531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;62C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F7,0202020202020202020202020202020202020202020202020202020202020202;0303030303030303030303030303030303030303030303030303030303030303;0404040404040404040404040404040404040404040404040404040404040404;0505050505050505050505050505050505050505050505050505050505050505;0606060606060606060606060606060606060606060606060606060606060606,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F;C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9233428AEB77468852520B0256DE84562A29629194DAEAC45444C7711CD379E57;F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F923A17EA0CD56ED3A824AA14BC67839CED465BA5269700F600EDFC3DAAC8A6B069;A3AFBDB45A6A34BF7C8C00F1B6D7E7D375B54540F13716C87B62E51E2F4F22FF3E57DF859581383FA53C564BC72EC8A60145C502E91B0B508A6F1BD7961FC45D;C1319F3743A9B8A5A30408FA90E96B3A8BB3FD4FF5B790935C465DDC68859B2935B87C950EB399CCCD956D5BE6327B164D4C4080671A6B2CB39FD6A755C5987E,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F92A3AFBDB45A6A34BF7C8C00F1B6D7E7D375B54540F13716C87B62E51E2F4F22FFC1319F3743A9B8A5A30408FA90E96B3A8BB3FD4FF5B790935C465DDC68859B293BCB7A264BF18758B66172F5A56AF98A0CEDAF944D094EE396A941A09F10015A,Five signatures +5,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766,0202020202020202020202020202020202020202020202020202020202020202;0303030303030303030303030303030303030303030303030303030303030303,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8A63C71B6F2AE3FFC8734B47B77A4F5F6C0274D7654BEC2C0D75E69A94892C04A;C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9DF1D0EE03A7E5D274A857BF080C8BD3B5244BC1A9D31F660B8E22F309260FF26,"Strange aggregation - invalid individual sigs, valid aggregate" +6,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,,Signature s = n (at boundary) +7,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364529,FALSE,,Signature s > n +8,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FALSE,,Signature s = 2^256 - 1 diff --git a/py-halfagg/vectors/test_vectors_incaggregate.csv b/py-halfagg/vectors/test_vectors_incaggregate.csv new file mode 100644 index 0000000..41f9298 --- /dev/null +++ b/py-halfagg/vectors/test_vectors_incaggregate.csv @@ -0,0 +1,9 @@ +index,aggsig,pm_aggd_pubkeys,pm_aggd_messages,pms_pubkeys,pms_messages,pms_signatures,expected_result,expected_aggsig,comment +0,0000000000000000000000000000000000000000000000000000000000000000,,,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,Add single signature to empty aggregate +1,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766,0303030303030303030303030303030303030303030303030303030303030303,C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9233428AEB77468852520B0256DE84562A29629194DAEAC45444C7711CD379E57,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9DF1D0EE03A7E5D274A857BF080C8BD3B5244BC1A9D31F660B8E22F309260FF26,Add second signature to single-sig aggregate +2,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766;531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337,0303030303030303030303030303030303030303030303030303030303030303;0404040404040404040404040404040404040404040404040404040404040404,C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9233428AEB77468852520B0256DE84562A29629194DAEAC45444C7711CD379E57;F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F923A17EA0CD56ED3A824AA14BC67839CED465BA5269700F600EDFC3DAAC8A6B069,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F92F69F0DB595934BE97771DEF114962A9277861BFD5858D2E364DCCF30266F8CD4,Add two signatures at once +3,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,,,,TRUE,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,Add no new signatures (unchanged) +4,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,,,,,,FALSE,,Aggregate signature wrong length +5,,,,,,,FALSE,,Aggregate signature empty +6,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,,,,,,FALSE,,Existing aggregate has s = n +7,0000000000000000000000000000000000000000000000000000000000000000,,,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,,New signature has s = n diff --git a/py-halfagg/vectors/test_vectors_verify.csv b/py-halfagg/vectors/test_vectors_verify.csv new file mode 100644 index 0000000..46d20b3 --- /dev/null +++ b/py-halfagg/vectors/test_vectors_verify.csv @@ -0,0 +1,23 @@ +index,aggsig,pubkeys,messages,expected_result,comment +0,0000000000000000000000000000000000000000000000000000000000000000,,,TRUE,Empty signature list +1,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,TRUE,Single signature +2,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8A3AFBDB45A6A34BF7C8C00F1B6D7E7D375B54540F13716C87B62E51E2F4F22FFBF8913EC53226A34892D60252A7052614CA79AE939986828D81D2311957371AD,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,0202020202020202020202020202020202020202020202020202020202020202;0505050505050505050505050505050505050505050505050505050505050505,TRUE,Two signatures +3,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F92F69F0DB595934BE97771DEF114962A9277861BFD5858D2E364DCCF30266F8CD4,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766;531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337,0202020202020202020202020202020202020202020202020202020202020202;0303030303030303030303030303030303030303030303030303030303030303;0404040404040404040404040404040404040404040404040404040404040404,TRUE,Three signatures +4,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9F62B58B1908197B6D732F0564AD2AD5C6E438D24C65AF758DA9ACCCCCF5B3F92A3AFBDB45A6A34BF7C8C00F1B6D7E7D375B54540F13716C87B62E51E2F4F22FFC1319F3743A9B8A5A30408FA90E96B3A8BB3FD4FF5B790935C465DDC68859B293BCB7A264BF18758B66172F5A56AF98A0CEDAF944D094EE396A941A09F10015A,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766;531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;62C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F7,0202020202020202020202020202020202020202020202020202020202020202;0303030303030303030303030303030303030303030303030303030303030303;0404040404040404040404040404040404040404040404040404040404040404;0505050505050505050505050505050505050505050505050505050505050505;0606060606060606060606060606060606060606060606060606060606060606,TRUE,Five signatures +5,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Public key not on curve +6,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,0000000000000000000000000000000000000000000000000000000000000000,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Public key is zero +7,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Public key >= field size +8,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D0E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,R value not on curve +9,00000000000000000000000000000000000000000000000000000000000000000E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,R value is zero +10,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC930E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,R value >= field size +11,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Aggregate s = n +12,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364529,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Aggregate s > n +13,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Aggregate s = 2^256 - 1 +14,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FALSE,Wrong message +15,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F,4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Wrong public key +16,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C2260,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Tampered signature (bit flip) +17,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8C9D9B1CCE8C0A99AA79F56185358CD82B7649D6132DAACB31612C7C9B14CE3A9DF1D0EE03A7E5D274A857BF080C8BD3B5244BC1A9D31F660B8E22F309260FF26,4D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766;1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0303030303030303030303030303030303030303030303030303030303030303;0202020202020202020202020202020202020202020202020202020202020202,FALSE,Swapped order +18,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,"s = n-1 (valid range, wrong sig)" +19,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80000000000000000000000000000000000000000000000000000000000000000,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,"s = 0 (valid range, wrong sig)" +20,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4A,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Aggregate signature too short +21,B070AAFCEA439A4F6F1BBFC2EB66D29D24B0CAB74D6B745C3CFB009CC8FE4AA80E066C34819936549FF49B6FD4D41EDFC401A367B87DDD59FEE38177961C225F0000000000000000000000000000000000000000000000000000000000000000,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,0202020202020202020202020202020202020202020202020202020202020202,FALSE,Aggregate signature too long