diff --git a/README.mediawiki b/README.mediawiki index 3c5db97075..c79b251d8e 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1513,6 +1513,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | bubb1es, haris | Specification | Draft +|- +| [[bip-0458.mediawiki|458]] +| Applications +| Half-Aggregation of BIP 340 Signatures +| Tim Ruffing, Jonas Nick, Fabian Jahr +| Specification +| Draft |} diff --git a/bip-0458.mediawiki b/bip-0458.mediawiki new file mode 100644 index 0000000000..2adda452fa --- /dev/null +++ b/bip-0458.mediawiki @@ -0,0 +1,216 @@ +
+ BIP: 458 + Layer: Applications + Title: Half-Aggregation of BIP 340 Signatures + Authors: Tim Ruffing+ +== Introduction == + +=== Abstract === + +This document describes ''half-aggregation'' of BIP 340 signatures. +Half-aggregation is a non-interactive process for aggregating a collection of signatures into a single aggregate signature. +The size of the resulting aggregate signature is approximately half of the combined size of the original signatures. + +=== Copyright === + +This document, along with the [[bip-0458/halfagg.py|reference implementation]] and [[bip-0458/vectors/|test vectors]], is licensed under the BSD-3-Clause License. The exception is the vendored copy of the secp256k1lab library in [[bip-0458/secp256k1lab/|bip-0458/secp256k1lab]], which is licensed under the MIT License as stated in its [[bip-0458/secp256k1lab/COPYING|COPYING]] file. + +=== Motivation === + +Half-aggregation is applicable if there is a verifier that needs to verify multiple signatures. +Instead of sending individual signatures to the verifier, the signatures can be compressed into a single aggregate signature and sent to the verifier. +If the verifier can successfully verify the aggregate signature, the verifier can be sure that the individual signatures would have passed verification. + +The purpose of half-aggregation is to reduce the size of the data that is sent to the verifier. +While ''n'' BIP 340 signatures are ''64*n'' bytes, a half-aggregate of the same signatures is ''32*n + 32'' bytes. +The process of half-aggregation is straightforward: it is a pure function of the input signatures, public keys, and messages. +It is non-interactive and does ''not'' require cooperation from other parties, including signers or verifiers. +This is unlike interactive multisignature schemes such as MuSig2 ([[bip-0327.mediawiki|BIP 327]]), in which signers cooperate to produce a single signature under a single aggregate public key. +In half-aggregation, there is no aggregate key, and the aggregate signature is verified against the individual public keys and messages. + +There are a variety of scenarios where half-aggregation of BIP 340 signatures is useful. +To keep this section brief and avoid getting outdated quickly, we focus on listing example applications and defer the detailed discussion of the application-specific trade-offs to other places. + +One example is the Lightning Network routing gossip protocol, which [https://github.com/lightning/bolts/blob/2e8f2095a36afb9de38da0f3f0051c7dc16dfc36/07-routing-gossip.md as of this writing] involves messages that contain ECDSA signatures. +If the signature scheme was changed to BIP 340, half-aggregation could reduce the total amount of gossiped data. +Instead of sending individual gossip messages, nodes could assemble a batch of messages and half-aggregate the signatures of the individual messages into a single signature for the batch. + +Another application of half-aggregation is within the Bitcoin consensus protocol. +In particular, it has been discussed in the context of the [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-February/015700.html Graftroot proposal]. +Half-aggregation would allow Graftroot spending to be as efficient as best-case Taproot spends by aggregating the signature of the ''surrogate script'' and signatures that satisfy this script. +Moreover, half-aggregation improves the efficiency of proposed Bitcoin script opcodes that verify multiple signatures, such as [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-February/019926.html OP_EVICT]. +We can also imagine adding a Bitcoin script opcode that verifies a half-aggregate signature, allowing for more efficient non-interactive multi and threshold signatures. + +Half-aggregation can also be applied to the signatures in the inputs of Bitcoin transactions, a process known as cross-input signature aggregation (CISA). +This [https://github.com/ElementsProject/cross-input-aggregation/blob/master/savings.org reduces the size of an average transaction] by 20.6% and the weight by 7.6%. +A known downside of using half-aggregation is that some uses of adaptor signature protocols [https://github.com/ElementsProject/cross-input-aggregation#half-aggregation-and-adaptor-signatures may be incompatible]. +Usually, CISA is proposed with interactive ''full'' signature aggregation instead of non-interactive half-aggregation because creating a valid transaction already requires cooperation, and full signature aggregation is more efficient. +However, the difference in complexity between half-aggregation and full aggregation is so significant that basing a CISA on half-aggregation is a legitimate approach. + +The most invasive application to Bitcoin's consensus would be block-wide signature aggregation. +It refers to a process where block producers aggregate as many transaction signatures as possible. +In the best case, a full block would only have a single half-aggregate signature. +While this is attractive from the efficiency perspective, block-wide aggregation requires more research (and, in particular, special attention to handling +[https://github.com/ElementsProject/cross-input-aggregation#half-aggregation-and-reorgs reorgs]). + +=== Rationale === + +The idea for half-aggregation of Schnorr signatures was brought up in the context of block-wide signature aggregation [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014272.html by Tadge Dryja on the Bitcoin mailing list] in 2017. +The scheme had a security flaw that was [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014306.html noticed] and [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014308.html fixed] shortly after by Russell O'Connor and Andrew Poelstra. +In 2021 [https://eprint.iacr.org/2021/350 Chalkias, Garillot, Kondi and Nikolaenko] published a security proof in the random oracle model (ROM) that reduces the security of half-aggregation to the security of Schnorr signatures. +[https://eprint.iacr.org/2022/222.pdf Chen and Zhao] were able to produce a tight proof in the ROM and algebraic group model in the following year. +Moreover, they came up with an elegant approach to incremental aggregation that is used in this document. + +* Incremental aggregation allows non-interactively aggregating additional BIP 340 signatures into an existing half-aggregate signature. +* A half-aggregate signature of ''u'' BIP 340 input signatures is serialized as the ''(u+1)⋅32''-byte array ''r1 || ... || ru || bytes(s)'' where ''ri'' is a 32-byte array from input signature ''i'' and ''s'' is a scalar aggregate (see below for details). +* This document does ''not'' specify the aggregation of multiple aggregate signatures (yet). It is possible, but requires changing the encoding of an aggregate signature. Since it is not possible to undo the aggregation of the s-values, when verifying of such an aggregate signature the randomizers need to be the same as when verifying the individual aggregate signature. Therefore, the aggregate signature needs to encode a tree that reveals how the individual signatures were aggregated and how the resulting aggregate signatures were reaggregated. +* The first randomizer ''z0'' is fixed to the constant ''1'', which speeds up verification because ''z0⋅R0 = R0''. This optimization has been suggested and proven secure by [https://eprint.iacr.org/2022/222.pdf Chen and Zhao]. +* This BIP does not impose a maximum number of signatures that can be aggregated in a single half-aggregate signature. Implementations may enforce limits, e.g., to facilitate testing, to prevent resource exhaustion and integer overflows. Additionally, applications should enforce a (possibly lower) limit that suits their needs, taking into account that processing large aggregates may take a considerable amount of time. We note that there must be agreement over the limits in consensus-critical applications. + +== Description == + +=== Specification === + +The specification is written in Python and can be found in [[bip-0458/halfagg.py|halfagg.py]]. +Note that the specification depends on a vendored copy of the [https://github.com/secp256k1lab/secp256k1lab secp256k1lab library]. + +=== Test Vectors === + +Test vectors are provided in [[bip-0458/vectors/|vectors]] and are generated with a [[bip-0458/gen_test_vectors.py|generator script]]. +The specification can be executed with the test vectors by running [[bip-0458/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 Python [[#specification|specification]]. + +==== Notation ==== + +The following conventions are used, with constants as defined for [https://www.secg.org/sec2-v2.pdf secp256k1]. We note that adapting this specification to other elliptic curves is not straightforward and can result in an insecure schemeAmong other pitfalls, using the specification with a curve whose order is not close to the size of the range of the nonce derivation function is insecure.. +* Lowercase variables represent integers or byte arrays. +** The constant ''p'' refers to the field size, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F''. +** The constant ''n'' refers to the curve order, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141''. +* Uppercase variables refer to points on the curve with equation ''y2 = x3 + 7'' over the integers modulo ''p''. +** ''is_infinite(P)'' returns whether or not ''P'' is the point at infinity. +** ''x(P)'' and ''y(P)'' are integers in the range ''0..p-1'' and refer to the X and Y coordinates of a point ''P'' (assuming it is not infinity). +** The constant ''G'' refers to the base point, for which ''x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'' and ''y(G) = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8''. +** Addition of points refers to the usual [https://en.wikipedia.org/wiki/Elliptic_curve#The_group_law elliptic curve group operation]. +** [https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication Multiplication (⋅) of an integer and a point] refers to the repeated application of the group operation. +* Functions and operations: +** ''||'' refers to byte array concatenation. +** The function ''x[i:j]'', where ''x'' is a byte array and ''i, j ≥ 0'', returns a ''(j - i)''-byte array with a copy of the ''i''-th byte (inclusive) to the ''j''-th byte (exclusive) of ''x''. +** The function ''bytes(x)'', where ''x'' is an integer, returns the 32-byte encoding of ''x'', most significant byte first. +** The function ''bytes(P)'', where ''P'' is a point, returns ''bytes(x(P))''. +** The function ''len(x)'' where ''x'' is a byte array returns the length of the array. +** The function ''int(x)'', where ''x'' is a 32-byte array, returns the 256-bit unsigned integer whose most significant byte first encoding is ''x''. +** The function ''has_even_y(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''y(P) mod 2 = 0''. +** The function ''lift_x(x)'', where ''x'' is a 256-bit unsigned integer, returns the point ''P'' for which ''x(P) = x'' + Given a candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''. The valid Y coordinates for a given candidate ''x'' are the square roots of ''c = x3 + 7 mod p'' and they can be computed as ''y = ±c(p+1)/4 mod p'' (see [https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus Quadratic residue]) if they exist, which can be checked by squaring and comparing with ''c''. and ''has_even_y(P)'', or fails if ''x'' is greater than ''p-1'' or no such point exists. The function ''lift_x(x)'' is equivalent to the following pseudocode: +*** Fail if ''x ≥ p''. +*** Let ''c = x3 + 7 mod p''. +*** Let ''y = c(p+1)/4 mod p''. +*** Fail if ''c ≠ y2 mod p''. +*** Return the unique point ''P'' such that ''x(P) = x'' and ''y(P) = y'' if ''y mod 2 = 0'' or ''y(P) = p-y'' otherwise. +** The function ''hashtag(x)'' where ''tag'' is a UTF-8 encoded tag name and ''x'' is a byte array returns the 32-byte hash ''SHA256(SHA256(tag) || SHA256(tag) || x)''. +* Other: +** Tuples are written by listing the elements within parentheses and separated by commas. For example, ''(2, 3, 1)'' is a tuple. + +In the [[#specification|reference implementation]], these operations map to the following [https://github.com/secp256k1lab/secp256k1lab secp256k1lab] library methods, where ''GE'' and ''Scalar'' represent points and scalars: + +{| class="wikitable" +! Notation !! secp256k1lab +|- +| ''lift_x(int(x))'' || ''GE.from_bytes_xonly(x)'' +|- +| ''int(x)'', failing if ''≥ n'' || ''Scalar.from_bytes_checked(x)'' +|- +| ''int(x) mod n'' || ''Scalar.from_bytes_wrapping(x)'' +|- +| ''bytes(s)'' || ''s.to_bytes()'' +|- +| ''hashtag(x)'' || ''tagged_hash(tag, x)'' +|} + +==== Aggregate ==== + +''Aggregate'' takes an array of public key, message and signature triples and returns an aggregate signature. +If every triple ''(p, m, s)'' is valid (i.e., ''Verify(p, m, s)'' as defined in BIP 340 returns true), then the returned aggregate signature and the array of ''(p, m)'' tuples passes ''VerifyAggregate''. +(However, the inverse does not hold: given suitable valid triples, it is possible to construct an input array to ''Aggregate'' which contains invalid triples, but for which ''VerifyAggregate'' will accept the aggregate signature returned by ''Aggregate''. If this is undesired, input triples should be verified individually before passing them to ''Aggregate''.) + +Input: +* ''pms0..u-1'': an array of ''u'' triples, where the first element of each triple is a 32-byte public key, the second element is a 32-byte message and the third element is a 64-byte BIP 340 signature + +'''''Aggregate(pms0..u-1)''''': +* Let ''aggsig = bytes(0)'' +* Let ''pm_aggd'' be an empty array +* Return ''IncAggregate(aggsig, pm_aggd, pms0..u-1)''; fail if that fails. + +==== IncAggregate ==== + +''IncAggregate'' takes an aggregate signature, an array of public key and message tuples corresponding to the aggregate signature, and an additional array of public key, message and signature triples. +It aggregates the additional array of triples into the existing aggregate signature and returns the resulting new aggregate signature. +In other words, if ''VerifyAggregate(aggsig, pm_aggd)'' passes and every triple ''(p, m, s)'' in ''pms_to_agg'' is valid (i.e., ''Verify(p, m, s)'' as defined in BIP 340 returns true), then the returned aggregate signature along with the array of ''(p, m)'' tuples of ''pm_aggd'' and ''pms_to_agg'' passes ''VerifyAggregate''. +(However, the inverse does not hold: given a suitable valid aggregate signature and suitable valid triples, it is possible to construct inputs to ''IncAggregate'' which contain an invalid aggregate signature or invalid triples, but for which ''VerifyAggregate'' will accept the aggregate signature returned by ''IncAggregate''. If this is undesired, the input triples and the input aggregate signature should be verified individually before passing them to ''IncAggregate''.) + +Input: +* ''aggsig'' : a byte array +* ''pm_aggd0..v-1'': an array of ''v'' tuples, where the first element of each tuple is a 32-byte public key and the second element is a 32-byte message +* ''pms_to_agg0..u-1'': an array of ''u'' triples, where the first element of each tuple is a 32-byte public key, the second element is a 32-byte message and the third element is a 64-byte BIP 340 signature + +'''''IncAggregate(aggsig, pm_aggd0..v-1, pms_to_agg0..u-1)''''': +* Fail if ''len(aggsig) ≠ 32 * (v + 1)'' +* For ''i = 0 .. v-1'': +** Let ''(pki, mi) = pm_aggdi'' +** Let ''ri = aggsig[i⋅32:(i+1)⋅32]'' +* For ''i = v .. v+u-1'': +** Let ''(pki, mi, sigi) = pms_to_aggi-v'' +** Let ''ri = sigi[0:32]'' +** Let ''si = int(sigi[32:64])'' +** If ''i = 0'': +*** Let ''zi = 1'' +** Else: +*** Let ''zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n'' +* Let ''s = int(aggsig[v⋅32:(v+1)⋅32]) + zv⋅sv + ... + zv+u-1⋅sv+u-1 mod n'' +* Return ''r0 || ... || rv+u-1 || bytes(s)'' + +==== VerifyAggregate ==== + +''VerifyAggregate'' verifies a given aggregate signature against an array of public key and message tuples. + +Input: +* ''aggsig'' : a byte array +* ''pm_aggd0..u-1'': an array of ''u'' tuples, where the first element of each tuple is a 32-byte public key and the second element is a 32-byte message + +'''''VerifyAggregate(aggsig, pm_aggd0..u-1)''''': +The algorithm ''VerifyAggregate(aggsig, pm_aggd0..u-1)'' is defined as: +* Fail if ''len(aggsig) ≠ 32 * (u + 1)'' +* For ''i = 0 .. u-1'': +** Let ''(pki, mi) = pm_aggdi'' +** Let ''Pi = lift_x(int(pki))''; fail if that fails +** Let ''ri = aggsig[i⋅32:(i+1)⋅32]'' +** Let ''Ri = lift_x(int(ri))''; fail if that fails +** Let ''ei = int(hashBIP0340/challenge(bytes(ri) || pki || mi)) mod n'' +** If ''i = 0'': +*** Let ''zi = 1'' +** Else: +*** Let ''zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n'' +* Let ''s = int(aggsig[u⋅32:(u+1)⋅32])''; fail if ''s ≥ n'' +* Fail if ''s⋅G ≠ z0⋅(R0 + e0⋅P0) + ... + zu-1⋅(Ru-1 + eu-1⋅Pu-1)'' +* Return success iff no failure occurred before reaching this point. + +The verification algorithm is similar to [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#batch-verification BIP340 Batch Verification]. As in BIP340, using an [https://bitcoin.stackexchange.com/a/80702/109853 efficient algorithm for computing the sum of multiple EC multiplications] can significantly speed up verification. + +== Backward Compatibility == + +Half-aggregation specifies a new, optional construction for aggregating existing BIP 340 signatures. +It does not change the validation of individual BIP 340 signatures and does not affect any existing Bitcoin consensus rules or protocol behavior. +Therefore, this BIP introduces no backward compatibility concerns. diff --git a/bip-0458/gen_test_vectors.py b/bip-0458/gen_test_vectors.py new file mode 100644 index 0000000000..6e297a3320 --- /dev/null +++ b/bip-0458/gen_test_vectors.py @@ -0,0 +1,647 @@ +#!/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 +from typing import List, Tuple + +from halfagg import Aggregate, IncAggregate, hashHalfAgg_randomizer +from secp256k1lab.secp256k1 import GE, FE, Scalar +from secp256k1lab.util import bytes_from_int +from secp256k1lab.bip340 import pubkey_gen, schnorr_sign, schnorr_verify + + +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 = Scalar.from_bytes_wrapping(aggsig_valid[64:96]) + s1_new = Scalar.from_int_wrapping(0x123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0) + s0_new = (sagg - z[1] * s1_new) / z[0] + + sig_a_invalid = sig_a[:32] + s0_new.to_bytes() + assert not schnorr_verify(msg_a, pk_a, sig_a_invalid), "sig_a_invalid should not verify" + sig_b_invalid = sig_b[:32] + s1_new.to_bytes() + 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/bip-0458/halfagg.py b/bip-0458/halfagg.py new file mode 100644 index 0000000000..ee4615dc00 --- /dev/null +++ b/bip-0458/halfagg.py @@ -0,0 +1,199 @@ +#!/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 pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src")) +from secp256k1lab.secp256k1 import ( + G, + GE, + Scalar, +) +from secp256k1lab.util import ( + tagged_hash, +) + + +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 = [] + + # 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. + """ + + v = len(pm_aggd) + u = len(pms_to_agg) + + # 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 = Scalar.from_bytes_checked(sigi[32:64]) + 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 = Scalar.from_bytes_checked(aggsig[v * 32:(v + 1) * 32]) + + for i in range(u): + s = s + z_values[i] * s_values[i] + + # Return r0 || ... || rv+u-1 || bytes(s) + return b''.join(r_values) + s.to_bytes() + + +# 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. + """ + + u = len(pm_aggd) + + # 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.from_bytes_xonly(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.from_bytes_xonly(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 = Scalar.from_bytes_wrapping(hashBIP0340_challenge(ri, pki, mi)) + 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 + try: + s = Scalar.from_bytes_checked(aggsig[u * 32:(u + 1) * 32]) + except ValueError: + return False + + # 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 = e_values[i] + P = P_values[i] + R = R_values[i] + rhsi = R + e * P + + z = 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 Scalar(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 Scalar.from_bytes_wrapping(tagged_hash("HalfAgg/randomizer", random_input)) diff --git a/bip-0458/run_test_vectors.py b/bip-0458/run_test_vectors.py new file mode 100644 index 0000000000..b496b6f6f8 --- /dev/null +++ b/bip-0458/run_test_vectors.py @@ -0,0 +1,161 @@ +#!/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 ValueError: + pass + else: + try: + actual = Aggregate(pms) + if actual != bytes.fromhex(expected_aggsig): + failures.append(f"Aggregate #{index}: {comment}") + except ValueError: + 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 ValueError: + pass + else: + try: + actual = IncAggregate(aggsig, pm_aggd, pms_to_agg) + if actual != bytes.fromhex(expected_aggsig): + failures.append(f"IncAggregate #{index}: {comment}") + except ValueError: + 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 ValueError: + if expected_result == "TRUE": + failures.append(f"VerifyAggregate #{index}: {comment}") + + 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()) + + 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/bip-0458/secp256k1lab/.github/workflows/main.yml b/bip-0458/secp256k1lab/.github/workflows/main.yml new file mode 100644 index 0000000000..fb05230b3c --- /dev/null +++ b/bip-0458/secp256k1lab/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: Tests +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx ruff check . + mypy: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv, setup Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + - run: uvx mypy . + unittest: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: python3 -m unittest diff --git a/bip-0458/secp256k1lab/.gitignore b/bip-0458/secp256k1lab/.gitignore new file mode 100644 index 0000000000..505a3b1ca2 --- /dev/null +++ b/bip-0458/secp256k1lab/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/bip-0458/secp256k1lab/.python-version b/bip-0458/secp256k1lab/.python-version new file mode 100644 index 0000000000..2c0733315e --- /dev/null +++ b/bip-0458/secp256k1lab/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/bip-0458/secp256k1lab/CHANGELOG.md b/bip-0458/secp256k1lab/CHANGELOG.md new file mode 100644 index 0000000000..4c756d3695 --- /dev/null +++ b/bip-0458/secp256k1lab/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +#### Added + - Added new methods `Scalar.from_int_nonzero_checked` and `Scalar.from_bytes_nonzero_checked` + that ensure a constructed scalar is in the range `0 < s < N` (i.e. is non-zero and within the + group order) and throw a `ValueError` otherwise. This is e.g. useful for ensuring that newly + generated secret keys or nonces are valid without having to do the non-zero check manually. + The already existing methods `Scalar.from_int_checked` and `Scalar.from_bytes_checked` error + on overflow, but not on zero, i.e. they only ensure `0 <= s < N`. + + - Added a new method `GE.from_bytes_compressed_with_infinity` to parse a compressed + public key (33 bytes) to a group element, where the all-zeros bytestring maps to the + point at infinity. This is the counterpart to the already existing serialization + method `GE.to_bytes_compressed_with_infinity`. + +## [1.0.0] - 2025-03-31 + +Initial release. diff --git a/bip-0458/secp256k1lab/COPYING b/bip-0458/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/bip-0458/secp256k1lab/COPYING @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2009-2024 The Bitcoin Core developers +Copyright (c) 2009-2024 Bitcoin Developers +Copyright (c) 2025- The secp256k1lab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bip-0458/secp256k1lab/README.md b/bip-0458/secp256k1lab/README.md new file mode 100644 index 0000000000..dbc9dbd04c --- /dev/null +++ b/bip-0458/secp256k1lab/README.md @@ -0,0 +1,13 @@ +secp256k1lab +============ + + + +An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education. + +Features: +* Low-level secp256k1 field and group arithmetic. +* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). +* ECDH key exchange. + +WARNING: The code in this library is slow and trivially vulnerable to side channel attacks. diff --git a/bip-0458/secp256k1lab/pyproject.toml b/bip-0458/secp256k1lab/pyproject.toml new file mode 100644 index 0000000000..68b927b384 --- /dev/null +++ b/bip-0458/secp256k1lab/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "secp256k1lab" +version = "1.0.0" +description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education" +readme = "README.md" +authors = [ + { name = "Pieter Wuille", email = "pieter@wuille.net" }, + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +maintainers = [ + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +requires-python = ">=3.11" +license = "MIT" +license-files = ["COPYING"] +keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Security :: Cryptography", +] +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0458/secp256k1lab/src/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0458/secp256k1lab/src/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/bip-0458/secp256k1lab/src/secp256k1lab/bip340.py @@ -0,0 +1,73 @@ +# The following functions are based on the BIP 340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + +from .secp256k1 import FE, GE, G +from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash + + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_xonly() + + +def schnorr_sign( + msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340" +) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand)) + P = d0 * G + assert not P.infinity + d = d0 if P.has_even_y() else GE.ORDER - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand)) + k0 = ( + int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg)) + % GE.ORDER + ) + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = k0 * G + assert not R.infinity + k = k0 if R.has_even_y() else GE.ORDER - k0 + e = ( + int_from_bytes( + tagged_hash( + tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg + ) + ) + % GE.ORDER + ) + sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER) + assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix) + return sig + + +def schnorr_verify( + msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340" +) -> bool: + if len(pubkey) != 32: + raise ValueError("The public key must be a 32-byte array.") + if len(sig) != 64: + raise ValueError("The signature must be a 64-byte array.") + try: + P = GE.from_bytes_xonly(pubkey) + except ValueError: + return False + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (r >= FE.SIZE) or (s >= GE.ORDER): + return False + e = ( + int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg)) + % GE.ORDER + ) + R = s * G - e * P + if R.infinity or (not R.has_even_y()) or (R.x != r): + return False + return True diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0458/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0458/secp256k1lab/src/secp256k1lab/ecdh.py @@ -0,0 +1,16 @@ +import hashlib + +from .secp256k1 import GE, Scalar + + +def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE: + """TODO""" + shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey) + assert not shared_secret.infinity # prime-order group + return shared_secret + + +def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes: + """TODO""" + shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey) + return hashlib.sha256(shared_secret.to_bytes_compressed()).digest() diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/keys.py b/bip-0458/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0458/secp256k1lab/src/secp256k1lab/keys.py @@ -0,0 +1,15 @@ +from .secp256k1 import GE, G +from .util import int_from_bytes + +# The following function is based on the BIP 327 reference implementation +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py + + +# Return the plain public key corresponding to a given secret key +def pubkey_gen_plain(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_compressed() diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/py.typed b/bip-0458/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/secp256k1.py b/bip-0458/secp256k1lab/src/secp256k1lab/secp256k1.py new file mode 100644 index 0000000000..0526878d91 --- /dev/null +++ b/bip-0458/secp256k1lab/src/secp256k1lab/secp256k1.py @@ -0,0 +1,483 @@ +# Copyright (c) 2022-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + +from __future__ import annotations +from typing import Self + +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE: int + + def __init__(self, a: int | Self = 0, b: int | Self = 1) -> None: + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, type(self)): + num = a._num + den = a._den + else: + assert isinstance(a, int) + num = a % self.SIZE + den = 1 + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE + else: + assert isinstance(b, int) + den = (den * b) % self.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num: int = num + self._den: int = den + + def __add__(self, a: int | Self) -> Self: + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented + + def __radd__(self, a: int) -> Self: + """Compute the sum of an integer and a field element.""" + return type(self)(a) + self + + @classmethod + def sum(cls, *es: Self) -> Self: + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) + + def __sub__(self, a: int | Self) -> Self: + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented + + def __rsub__(self, a: int) -> Self: + """Compute the difference of an integer and a field element.""" + return type(self)(a) - self + + def __mul__(self, a: int | Self) -> Self: + """Compute the product of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented + + def __rmul__(self, a: int) -> Self: + """Compute the product of an integer with a field element.""" + return type(self)(a) * self + + def __truediv__(self, a: int | Self) -> Self: + """Compute the ratio of two field elements (second may be int).""" + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented + + def __pow__(self, a: int) -> Self: + """Raise a field element to an integer power.""" + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) + + def __neg__(self) -> Self: + """Negate a field element.""" + return type(self)(-self._num, self._den) + + def __int__(self) -> int: + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE + self._den = 1 + return self._num + + def sqrt(self) -> Self | None: + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError + + def is_square(self) -> bool: + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self) -> bool: + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a: object) -> bool: + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + elif isinstance(a, int): + return (self._num - self._den * a) % self.SIZE == 0 + return False # for other types + + def to_bytes(self) -> bytes: + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @classmethod + def from_int_checked(cls, v: int) -> Self: + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v: int) -> Self: + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b: bytes) -> Self: + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b: bytes) -> Self: + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) + + def __str__(self) -> str: + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self) -> str: + """Get a string representation of this field element.""" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self) -> Self | None: + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + @classmethod + def from_int_nonzero_checked(cls, v: int) -> Self: + """Convert an integer to a scalar (no zero or overflow allowed).""" + if not (0 < v < cls.SIZE): + raise ValueError + return cls(v) + + @classmethod + def from_bytes_nonzero_checked(cls, b: bytes) -> Self: + """Convert a 32-byte array to a scalar (BE byte order, no zero or overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_nonzero_checked(v) + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + GE objects are immutable. + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = Scalar.SIZE + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + @property + def infinity(self) -> bool: + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self) -> FE: + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self) -> FE: + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + + def __init__(self, x: int | FE | None = None, y: int | FE | None = None) -> None: + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self._infinity = True + else: + # Initialize as point on the curve (and check that it is). + assert x is not None + assert y is not None + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self._infinity = False + self._x = fx + self._y = fy + + def __add__(self, a: GE) -> GE: + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def sum(*ps: GE) -> GE: + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps: tuple[Scalar, GE]) -> GE: + """Compute a (batch) scalar group element multiplication. + + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(int(a), p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a: int | Scalar) -> GE: + """Multiply an integer or scalar with a group element.""" + if self == G: + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) + + def __neg__(self) -> GE: + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def __sub__(self, a: GE) -> GE: + """Subtract a group element from another.""" + return self + (-a) + + def __eq__(self, a: object) -> bool: + """Check if two group elements are equal.""" + if not isinstance(a, type(self)): + return False + return (self - a).infinity + + def has_even_y(self) -> bool: + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + + def to_bytes_compressed(self) -> bytes: + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_compressed_with_infinity(self) -> bytes: + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + + def to_bytes_uncompressed(self) -> bytes: + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self) -> bytes: + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x: int | FE) -> GE: + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + raise ValueError + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes_compressed(b: bytes) -> GE: + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_compressed_with_infinity(b: bytes) -> GE: + """Convert a compressed to a group element, mapping zeros to infinity.""" + if b == 33 * b"\x00": + return GE() + else: + return GE.from_bytes_compressed(b) + + @staticmethod + def from_bytes_uncompressed(b: bytes) -> GE: + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + + @staticmethod + def from_bytes(b: bytes) -> GE: + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + return GE.from_bytes_compressed(b) + else: + return GE.from_bytes_uncompressed(b) + + @staticmethod + def from_bytes_xonly(b: bytes) -> GE: + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r + + @staticmethod + def is_valid_x(x: int | FE) -> bool: + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self) -> str: + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self) -> str: + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + + def __hash__(self) -> int: + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p: GE) -> None: + self.table: list[GE] = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a: Scalar | int) -> GE: + result = GE() + a_ = int(a) + for bit in range(a_.bit_length()): + if a_ & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) diff --git a/bip-0458/secp256k1lab/src/secp256k1lab/util.py b/bip-0458/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0458/secp256k1lab/src/secp256k1lab/util.py @@ -0,0 +1,24 @@ +import hashlib + + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() diff --git a/bip-0458/secp256k1lab/test/__init__.py b/bip-0458/secp256k1lab/test/__init__.py new file mode 100644 index 0000000000..862ed6e21c --- /dev/null +++ b/bip-0458/secp256k1lab/test/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path +import sys + +# Ensure secp256k1lab is found and can be imported directly +sys.path.insert(0, str(Path(__file__).parent / "../src/")) diff --git a/bip-0458/secp256k1lab/test/test_bip340.py b/bip-0458/secp256k1lab/test/test_bip340.py new file mode 100644 index 0000000000..7fafad54bd --- /dev/null +++ b/bip-0458/secp256k1lab/test/test_bip340.py @@ -0,0 +1,51 @@ +import csv +from pathlib import Path +from random import randbytes +import unittest + +from secp256k1lab.bip340 import pubkey_gen, schnorr_sign, schnorr_verify + + +class BIP340Tests(unittest.TestCase): + """Test schnorr signatures (BIP 340).""" + + def test_correctness(self): + seckey = randbytes(32) + pubkey_xonly = pubkey_gen(seckey) + aux_rand = randbytes(32) + message = b'this is some arbitrary message' + signature = schnorr_sign(message, seckey, aux_rand) + success = schnorr_verify(message, pubkey_xonly, signature) + self.assertTrue(success) + + def test_vectors(self): + # Test against vectors from the BIPs repository + # [https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv] + vectors_file = Path(__file__).parent / "vectors" / "bip340.csv" + with open(vectors_file, encoding='utf8') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + with self.subTest(i=int(row['index'])): + self.subtest_vectors_case(row) + + def subtest_vectors_case(self, row): + seckey = bytes.fromhex(row['secret key']) + pubkey_xonly = bytes.fromhex(row['public key']) + aux_rand = bytes.fromhex(row['aux_rand']) + msg = bytes.fromhex(row['message']) + sig = bytes.fromhex(row['signature']) + result_str = row['verification result'] + comment = row['comment'] + + result = result_str == 'TRUE' + assert result or result_str == 'FALSE' + if seckey != b'': + pubkey_xonly_actual = pubkey_gen(seckey) + self.assertEqual(pubkey_xonly.hex(), pubkey_xonly_actual.hex(), f"BIP340 test vector ({comment}): pubkey mismatch") + sig_actual = schnorr_sign(msg, seckey, aux_rand) + self.assertEqual(sig.hex(), sig_actual.hex(), f"BIP340 test vector ({comment}): sig mismatch") + result_actual = schnorr_verify(msg, pubkey_xonly, sig) + if result: + self.assertEqual(result, result_actual, f"BIP340 test vector ({comment}): verification failed unexpectedly") + else: + self.assertEqual(result, result_actual, f"BIP340 test vector ({comment}): verification succeeded unexpectedly") diff --git a/bip-0458/secp256k1lab/test/test_ecdh.py b/bip-0458/secp256k1lab/test/test_ecdh.py new file mode 100644 index 0000000000..63c9da7a1b --- /dev/null +++ b/bip-0458/secp256k1lab/test/test_ecdh.py @@ -0,0 +1,18 @@ +from random import randbytes +import unittest + +from secp256k1lab.ecdh import ecdh_libsecp256k1 +from secp256k1lab.keys import pubkey_gen_plain + + +class ECDHTests(unittest.TestCase): + """Test ECDH module.""" + + def test_correctness(self): + seckey_alice = randbytes(32) + pubkey_alice = pubkey_gen_plain(seckey_alice) + seckey_bob = randbytes(32) + pubkey_bob = pubkey_gen_plain(seckey_bob) + shared_secret1 = ecdh_libsecp256k1(seckey_alice, pubkey_bob) + shared_secret2 = ecdh_libsecp256k1(seckey_bob, pubkey_alice) + self.assertEqual(shared_secret1, shared_secret2) diff --git a/bip-0458/secp256k1lab/test/test_secp256k1.py b/bip-0458/secp256k1lab/test/test_secp256k1.py new file mode 100644 index 0000000000..c6aee19a0a --- /dev/null +++ b/bip-0458/secp256k1lab/test/test_secp256k1.py @@ -0,0 +1,180 @@ +"""Test low-level secp256k1 field and group arithmetic classes.""" +from random import randint +import unittest + +from secp256k1lab.secp256k1 import FE, G, GE, Scalar + + +class PrimeFieldTests(unittest.TestCase): + def test_fe_constructors(self): + P = FE.SIZE + random_fe_valid = randint(0, P-1) + random_fe_overflowing = randint(P, 2**256-1) + + # wrapping constructors + for init_value in [0, P-1, P, P+1, random_fe_valid, random_fe_overflowing]: + fe1 = FE(init_value) + fe2 = FE.from_int_wrapping(init_value) + fe3 = FE.from_bytes_wrapping(init_value.to_bytes(32, 'big')) + reduced_value = init_value % P + self.assertEqual(int(fe1), reduced_value) + self.assertEqual(int(fe1), int(fe2)) + self.assertEqual(int(fe2), int(fe3)) + + # checking constructors (should throw on overflow) + for valid_value in [0, P-1, random_fe_valid]: + fe1 = FE.from_int_checked(valid_value) + fe2 = FE.from_bytes_checked(valid_value.to_bytes(32, 'big')) + self.assertEqual(int(fe1), valid_value) + self.assertEqual(int(fe1), int(fe2)) + + for overflow_value in [P, P+1, random_fe_overflowing]: + with self.assertRaises(ValueError): + _ = FE.from_int_checked(overflow_value) + with self.assertRaises(ValueError): + _ = FE.from_bytes_checked(overflow_value.to_bytes(32, 'big')) + + def test_scalar_constructors(self): + N = Scalar.SIZE + random_scalar_valid = randint(0, N-1) + random_scalar_overflowing = randint(N, 2**256-1) + + # wrapping constructors + for init_value in [0, N-1, N, N+1, random_scalar_valid, random_scalar_overflowing]: + s1 = Scalar(init_value) + s2 = Scalar.from_int_wrapping(init_value) + s3 = Scalar.from_bytes_wrapping(init_value.to_bytes(32, 'big')) + reduced_value = init_value % N + self.assertEqual(int(s1), reduced_value) + self.assertEqual(int(s1), int(s2)) + self.assertEqual(int(s2), int(s3)) + + # checking constructors (should throw on overflow) + for valid_value in [0, N-1, random_scalar_valid]: + s1 = Scalar.from_int_checked(valid_value) + s2 = Scalar.from_bytes_checked(valid_value.to_bytes(32, 'big')) + self.assertEqual(int(s1), valid_value) + self.assertEqual(int(s1), int(s2)) + + for overflow_value in [N, N+1, random_scalar_overflowing]: + with self.assertRaises(ValueError): + _ = Scalar.from_int_checked(overflow_value) + with self.assertRaises(ValueError): + _ = Scalar.from_bytes_checked(overflow_value.to_bytes(32, 'big')) + + # non-zero checking constructors (should throw on zero or overflow, only for Scalar) + random_nonzero_scalar_valid = randint(1, N-1) + for valid_value in [1, N-1, random_nonzero_scalar_valid]: + s1 = Scalar.from_int_nonzero_checked(valid_value) + s2 = Scalar.from_bytes_nonzero_checked(valid_value.to_bytes(32, 'big')) + self.assertEqual(int(s1), valid_value) + self.assertEqual(int(s1), int(s2)) + + for invalid_value in [0, N, random_scalar_overflowing]: + with self.assertRaises(ValueError): + _ = Scalar.from_int_nonzero_checked(invalid_value) + with self.assertRaises(ValueError): + _ = Scalar.from_bytes_nonzero_checked(invalid_value.to_bytes(32, 'big')) + + +class GeSerializationTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.point_at_infinity = GE() + cls.group_elements_on_curve = [ + # generator point + G, + # Bitcoin genesis block public key + GE(0x678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6, + 0x49f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f), + ] + # generate a few random points, to likely cover both even/odd y polarity + cls.group_elements_on_curve.extend([randint(1, Scalar.SIZE-1) * G for _ in range(8)]) + # generate x coordinates that don't have a valid point on the curve + # (note that ~50% of all x coordinates are valid, so finding one needs two loop iterations on average) + cls.x_coords_not_on_curve = [] + while len(cls.x_coords_not_on_curve) < 8: + x = randint(0, FE.SIZE-1) + if not GE.is_valid_x(x): + cls.x_coords_not_on_curve.append(x) + + cls.group_elements = [cls.point_at_infinity] + cls.group_elements_on_curve + + def test_infinity_raises(self): + with self.assertRaises(AssertionError): + _ = self.point_at_infinity.to_bytes_uncompressed() + with self.assertRaises(AssertionError): + _ = self.point_at_infinity.to_bytes_compressed() + with self.assertRaises(AssertionError): + _ = self.point_at_infinity.to_bytes_xonly() + + def test_not_on_curve_raises(self): + # for compressed and x-only GE deserialization, test with invalid x coordinate + for x in self.x_coords_not_on_curve: + x_bytes = x.to_bytes(32, 'big') + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed(b'\x02' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed(b'\x03' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed_with_infinity(b'\x02' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed_with_infinity(b'\x03' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_xonly(x_bytes) + + # for uncompressed GE serialization, test by invalidating either coordinate + for ge in self.group_elements_on_curve: + valid_x = ge.x + valid_y = ge.y + invalid_x = ge.x + 1 + invalid_y = ge.y + 1 + + # valid cases (if point (x,y) is on the curve, then point(x,-y) is on the curve as well) + _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + valid_y.to_bytes()) + _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + (-valid_y).to_bytes()) + # invalid cases (curve equation y**2 = x**3 + 7 doesn't hold) + self.assertNotEqual(invalid_y**2, valid_x**3 + 7) + with self.assertRaises(ValueError): + _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + invalid_y.to_bytes()) + self.assertNotEqual(valid_y**2, invalid_x**3 + 7) + with self.assertRaises(ValueError): + _ = GE.from_bytes_uncompressed(b'\x04' + invalid_x.to_bytes() + valid_y.to_bytes()) + + def test_affine(self): + # GE serialization and parsing round-trip (variants that only support serializing points on the curve) + for ge_orig in self.group_elements_on_curve: + # uncompressed serialization: 65 bytes, starts with 0x04 + ge_ser = ge_orig.to_bytes_uncompressed() + self.assertEqual(len(ge_ser), 65) + self.assertEqual(ge_ser[0], 0x04) + ge_deser = GE.from_bytes_uncompressed(ge_ser) + self.assertEqual(ge_deser, ge_orig) + + # compressed serialization: 33 bytes, starts with 0x02 (if y is even) or 0x03 (if y is odd) + ge_ser = ge_orig.to_bytes_compressed() + self.assertEqual(len(ge_ser), 33) + self.assertEqual(ge_ser[0], 0x02 if ge_orig.has_even_y() else 0x03) + ge_deser = GE.from_bytes_compressed(ge_ser) + self.assertEqual(ge_deser, ge_orig) + + # x-only serialization: 32 bytes + ge_ser = ge_orig.to_bytes_xonly() + self.assertEqual(len(ge_ser), 32) + ge_deser = GE.from_bytes_xonly(ge_ser) + if not ge_orig.has_even_y(): # x-only implies even y, so flip if necessary + ge_deser = -ge_deser + self.assertEqual(ge_deser, ge_orig) + + def test_affine_with_infinity(self): + # GE serialization and parsing round-trip (variants that also support serializing the point at infinity) + for ge_orig in self.group_elements: + # compressed serialization: 33 bytes, all-zeros for point at infinity + ge_ser = ge_orig.to_bytes_compressed_with_infinity() + self.assertEqual(len(ge_ser), 33) + if ge_orig.infinity: + self.assertEqual(ge_ser, b'\x00'*33) + else: + self.assertEqual(ge_ser[0], 0x02 if ge_orig.has_even_y() else 0x03) + ge_deser = GE.from_bytes_compressed_with_infinity(ge_ser) + self.assertEqual(ge_deser, ge_orig) diff --git a/bip-0458/secp256k1lab/test/vectors/bip340.csv b/bip-0458/secp256k1lab/test/vectors/bip340.csv new file mode 100644 index 0000000000..aa317a3b3d --- /dev/null +++ b/bip-0458/secp256k1lab/test/vectors/bip340.csv @@ -0,0 +1,20 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/bip-0458/vectors/test_vectors_aggregate.csv b/bip-0458/vectors/test_vectors_aggregate.csv new file mode 100644 index 0000000000..bdedae7f48 --- /dev/null +++ b/bip-0458/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/bip-0458/vectors/test_vectors_incaggregate.csv b/bip-0458/vectors/test_vectors_incaggregate.csv new file mode 100644 index 0000000000..41f9298bbb --- /dev/null +++ b/bip-0458/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/bip-0458/vectors/test_vectors_verify.csv b/bip-0458/vectors/test_vectors_verify.csv new file mode 100644 index 0000000000..46d20b37ac --- /dev/null +++ b/bip-0458/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+ Jonas Nick + Fabian Jahr + Status: Draft + Type: Specification + Assigned: 2026-07-02 + License: BSD-3-Clause + Discussion: 2022-07-08: https://gnusha.org/pi/bitcoindev/33f275c2-06b1-4b4a-2a75-cafe36836503%40gmail.com/ + Requires: 340 +