From 98b2bbdf6dcd88667ba83d49e1c4461d6338dfab Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 11 Jul 2026 18:00:37 +0200 Subject: [PATCH 1/3] BIP459: Add vendored secp256k1lab library Add a vendored copy of the secp256k1lab library (master branch, commit a265da139aea27386085a2a8760f8698e1bda64e) that the full-aggregation reference implementation and test vector scripts depend on. --- .../secp256k1lab/.github/workflows/main.yml | 34 ++ bip-0459/secp256k1lab/.gitignore | 10 + bip-0459/secp256k1lab/.python-version | 1 + bip-0459/secp256k1lab/CHANGELOG.md | 25 + bip-0459/secp256k1lab/COPYING | 23 + bip-0459/secp256k1lab/README.md | 13 + bip-0459/secp256k1lab/pyproject.toml | 34 ++ .../secp256k1lab/src/secp256k1lab/__init__.py | 0 .../secp256k1lab/src/secp256k1lab/bip340.py | 73 +++ .../secp256k1lab/src/secp256k1lab/ecdh.py | 16 + .../secp256k1lab/src/secp256k1lab/keys.py | 15 + .../secp256k1lab/src/secp256k1lab/py.typed | 0 .../src/secp256k1lab/secp256k1.py | 483 ++++++++++++++++++ .../secp256k1lab/src/secp256k1lab/util.py | 24 + bip-0459/secp256k1lab/test/__init__.py | 5 + bip-0459/secp256k1lab/test/test_bip340.py | 51 ++ bip-0459/secp256k1lab/test/test_ecdh.py | 18 + bip-0459/secp256k1lab/test/test_secp256k1.py | 180 +++++++ bip-0459/secp256k1lab/test/vectors/bip340.csv | 20 + 19 files changed, 1025 insertions(+) create mode 100644 bip-0459/secp256k1lab/.github/workflows/main.yml create mode 100644 bip-0459/secp256k1lab/.gitignore create mode 100644 bip-0459/secp256k1lab/.python-version create mode 100644 bip-0459/secp256k1lab/CHANGELOG.md create mode 100644 bip-0459/secp256k1lab/COPYING create mode 100644 bip-0459/secp256k1lab/README.md create mode 100644 bip-0459/secp256k1lab/pyproject.toml create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/__init__.py create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/bip340.py create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/ecdh.py create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/keys.py create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/py.typed create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/secp256k1.py create mode 100644 bip-0459/secp256k1lab/src/secp256k1lab/util.py create mode 100644 bip-0459/secp256k1lab/test/__init__.py create mode 100644 bip-0459/secp256k1lab/test/test_bip340.py create mode 100644 bip-0459/secp256k1lab/test/test_ecdh.py create mode 100644 bip-0459/secp256k1lab/test/test_secp256k1.py create mode 100644 bip-0459/secp256k1lab/test/vectors/bip340.csv diff --git a/bip-0459/secp256k1lab/.github/workflows/main.yml b/bip-0459/secp256k1lab/.github/workflows/main.yml new file mode 100644 index 0000000000..fb05230b3c --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/.gitignore b/bip-0459/secp256k1lab/.gitignore new file mode 100644 index 0000000000..505a3b1ca2 --- /dev/null +++ b/bip-0459/secp256k1lab/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/bip-0459/secp256k1lab/.python-version b/bip-0459/secp256k1lab/.python-version new file mode 100644 index 0000000000..2c0733315e --- /dev/null +++ b/bip-0459/secp256k1lab/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/bip-0459/secp256k1lab/CHANGELOG.md b/bip-0459/secp256k1lab/CHANGELOG.md new file mode 100644 index 0000000000..4c756d3695 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/COPYING b/bip-0459/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/README.md b/bip-0459/secp256k1lab/README.md new file mode 100644 index 0000000000..dbc9dbd04c --- /dev/null +++ b/bip-0459/secp256k1lab/README.md @@ -0,0 +1,13 @@ +secp256k1lab +============ + +![Dependencies: None](https://img.shields.io/badge/dependencies-none-success) + +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-0459/secp256k1lab/pyproject.toml b/bip-0459/secp256k1lab/pyproject.toml new file mode 100644 index 0000000000..68b927b384 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0459/secp256k1lab/src/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0459/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0459/secp256k1lab/src/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0459/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/src/secp256k1lab/keys.py b/bip-0459/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/src/secp256k1lab/py.typed b/bip-0459/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0459/secp256k1lab/src/secp256k1lab/secp256k1.py b/bip-0459/secp256k1lab/src/secp256k1lab/secp256k1.py new file mode 100644 index 0000000000..0526878d91 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/src/secp256k1lab/util.py b/bip-0459/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/test/__init__.py b/bip-0459/secp256k1lab/test/__init__.py new file mode 100644 index 0000000000..862ed6e21c --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/test/test_bip340.py b/bip-0459/secp256k1lab/test/test_bip340.py new file mode 100644 index 0000000000..7fafad54bd --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/test/test_ecdh.py b/bip-0459/secp256k1lab/test/test_ecdh.py new file mode 100644 index 0000000000..63c9da7a1b --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/test/test_secp256k1.py b/bip-0459/secp256k1lab/test/test_secp256k1.py new file mode 100644 index 0000000000..c6aee19a0a --- /dev/null +++ b/bip-0459/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-0459/secp256k1lab/test/vectors/bip340.csv b/bip-0459/secp256k1lab/test/vectors/bip340.csv new file mode 100644 index 0000000000..aa317a3b3d --- /dev/null +++ b/bip-0459/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) From 57ee5acdec070df24c898dee7bd643227a2364d1 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 11 Jul 2026 18:01:28 +0200 Subject: [PATCH 2/3] BIP459: Add full-aggregation of BIP 340 signatures --- bip-0459.mediawiki | 431 ++++++++++++++++ bip-0459/gen_test_vectors.py | 466 ++++++++++++++++++ bip-0459/reference.py | 194 ++++++++ bip-0459/run_test_vectors.py | 246 +++++++++ .../test_vectors_partial_sig_verify.csv | 10 + bip-0459/vectors/test_vectors_sign.csv | 8 + bip-0459/vectors/test_vectors_sign_error.csv | 5 + bip-0459/vectors/test_vectors_tweak.csv | 5 + bip-0459/vectors/test_vectors_verify.csv | 16 + 9 files changed, 1381 insertions(+) create mode 100644 bip-0459.mediawiki create mode 100644 bip-0459/gen_test_vectors.py create mode 100644 bip-0459/reference.py create mode 100644 bip-0459/run_test_vectors.py create mode 100644 bip-0459/vectors/test_vectors_partial_sig_verify.csv create mode 100644 bip-0459/vectors/test_vectors_sign.csv create mode 100644 bip-0459/vectors/test_vectors_sign_error.csv create mode 100644 bip-0459/vectors/test_vectors_tweak.csv create mode 100644 bip-0459/vectors/test_vectors_verify.csv diff --git a/bip-0459.mediawiki b/bip-0459.mediawiki new file mode 100644 index 0000000000..d5ac48292c --- /dev/null +++ b/bip-0459.mediawiki @@ -0,0 +1,431 @@ +
+  BIP: 459
+  Layer: Applications
+  Title: Full-Aggregation of BIP 340 Signatures
+  Authors: Fabian Jahr 
+  Status: Draft
+  Type: Specification
+  Assigned: 2026-07-10
+  License: BSD-3-Clause
+  Discussion: 2025-04-17: https://groups.google.com/g/bitcoindev/c/eothFkxAvK0/m/IZVGezcdDAAJ
+              2026-07-06: https://groups.google.com/g/bitcoindev/c/TF5mPfy58RQ/m/lvSC2FdYAAAJ
+  Requires: 340
+
+ +== Introduction == + +=== Abstract === + +This document describes ''full-aggregation'' of BIP 340 signatures, a standard for the DahLIASJonas Nick, Tim Ruffing, Yannick Seurin: ''DahLIAS: Discrete Logarithm-Based Interactive Aggregate Signatures''. Cryptology ePrint Archive, Report 2025/692. https://eprint.iacr.org/2025/692 interactive aggregate signature scheme. +Full-aggregation is an interactive process for aggregating a collection of signatures into a single aggregate signature. +The resulting aggregate signature has the same size as a single BIP 340 signature (64 bytes), regardless of the number of signers. + +=== Copyright === + +This document, along with the [[bip-0459/reference.py|reference implementation]] and [[bip-0459/vectors/|test vectors]], is licensed under the BSD-3-Clause License. The exception is the vendored copy of the secp256k1lab library in [[bip-0459/secp256k1lab/|bip-0459/secp256k1lab]], which is licensed under the MIT License as stated in its [[bip-0459/secp256k1lab/COPYING|COPYING]] file. + +=== Motivation === + +Full-aggregation applies whenever a verifier must check multiple signatures. +The signatures are compressed into a single constant-size aggregate signature, and a verifier that accepts the aggregate is assured that each individual signature would have passed verification. + +The primary purpose is to reduce the amount of data sent to the verifier. +While ''n'' BIP 340 signatures are ''64*n'' bytes, a fully aggregated signature is always 64 bytes. +Verifying the aggregate is also faster than verifying each signature separately. +One application is cross-input signature aggregation (CISA) within the Bitcoin consensus protocol, which reduces the size and verification cost of multi-input transactions. +Deploying CISA requires a consensus change that this document does not specify. +This document only specifies the signature scheme and consensus integration is left to a future proposal. + +Full-aggregation is interactive. +It requires cooperation among all signers, following a two-round protocol whose first round can be preprocessed before the messages to be signed are known. +This makes full-aggregation unsuitable for scenarios where signers cannot coordinate. +However, when all input owners already cooperate to construct and sign a transaction, the interaction is not a significant burden. +Historical average transactions would see an [https://hrf.org/latest/cisa-research-paper/ average size reduction of 20.5% and a weight reduction of 7.3%]. +Full-aggregation is particularly beneficial for large transactions such as CoinJoins, PayJoins and consolidations, because the savings grow with the number of inputs. +For example, a hypothetical CoinJoin of 300 inputs and 300 outputs would see a size reduction of 42.7% and a weight reduction of 15.9%. +A typical consolidation transaction of 150 inputs and one output would see a weight reduction of 27.4%. +CISA with full-aggregation can even reduce the cost of privacy since participating in CoinJoin and PayJoin transactions can become cheaper than sending individual transactions. + +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 for a single message, full-aggregation does not combine the signers' keys. +Each signer signs their own message under their own public key, and the aggregate signature is verified against the list of individual public key and message pairs. + +== Overview == + +Implementers must make sure to understand this section thoroughly to avoid subtle mistakes that may lead to catastrophic failure. + +=== General Signing Flow === + +The signing protocol involves ''u'' signers, each with their own secret key, public key, and message, and a ''coordinator'' who aggregates their contributions. + +The basic order of operations to create a fully aggregated signature is as follows: + +'''First broadcast round:''' +The signers start the signing session by running ''NonceGen'' to compute ''secnonce'' and ''pubnonce''.We treat the ''secnonce'' and ''pubnonce'' as grammatically singular even though they include serializations of two scalars and two elliptic curve points, respectively. +Then, the signers send their ''pubnonce'' to the coordinator. +The coordinator runs ''NonceAgg'' to compute an aggregate nonce ''aggnonce'' and sends it back to the signers, along with the ordered list of all ''(pk, m, pubnonce)'' triples. + +'''Second broadcast round:''' +At this point, every signer has the required data to sign. +Every signer computes a partial signature by running ''Sign'' with their secret key, their ''secnonce'', the message they intend to sign, and the session parameters. +Then, the signers send their partial signatures to the coordinator, who runs ''SigAgg'' to obtain the final 64-byte aggregate signature. +If all signers behaved honestly, the result passes ''Verify''. +If the aggregate signature is invalid, the coordinator can run ''PartialSigVerify'' on each partial signature to identify the disruptive signer. + +'''IMPORTANT''': The ''Sign'' algorithm must '''not''' be executed twice with the same ''secnonce''. +Otherwise, it is possible to extract the signer's secret key from the two partial signatures output by the two executions of ''Sign''. +To avoid accidental reuse of ''secnonce'', an implementation may securely erase the ''secnonce'' argument by overwriting it with 64 zero bytes after it has been read by ''Sign''. +A ''secnonce'' consisting of only zero bytes is invalid for ''Sign'' and will cause it to fail. + +To simplify the specification of the algorithms, some intermediary values are unnecessarily recomputed from scratch, e.g., when executing ''GetSessionValues'' multiple times. +Actual implementations can cache these values. +As a result, the session parameters may look different in implementations or may not exist as an explicit data structure. +However, computation of ''GetSessionValues'' and storage of the result must be protected against modification from an untrusted third party. + +=== Nonce Generation === + +'''IMPORTANT''': ''NonceGen'' must have access to a high-quality random generator to draw an unbiased, uniformly random value ''rand' ''. +In contrast to BIP 340 signing, the nonce values '''must not be derived deterministically''' from the signing key and message because otherwise active adversaries can trick the victim into reusing a nonce.An attacker who participates as a co-signer can run multiple signing sessions with the victim for the same message while varying its own nonce contributions. With deterministic derivation the victim reuses its secret nonces across these sessions while the aggregate nonce and therefore the nonce coefficient ''b'' and the challenge change. Three such sessions yield a system of linear equations that reveals the victim's secret key. Deriving the nonces from additional session data does not prevent this since the other signers' nonces are unknown to the victim when its nonces are generated. + +The optional arguments to ''NonceGen'' enable a defense-in-depth mechanism that may prevent secret key exposure if ''rand' '' is accidentally not drawn uniformly at random. +If the value ''rand' '' was identical in two ''NonceGen'' invocations but any other argument was different, the ''secnonce'' would still be different, avoiding accidental nonce reuse. +Therefore, it is recommended to provide the optional argument ''sk'' if available during nonce generation. +The auxiliary input ''extra_in'' can contain additional contextual data that has a chance of changing between ''NonceGen'' runs, e.g., a session identifier or a counter. +However, the protection provided by the optional arguments should only be viewed as a last resort. + +In some applications, it is beneficial to generate and send a ''pubnonce'' before the other signers, their public keys, or the messages to sign are known. +In this case, the ''NonceGen'' algorithm is run during a preprocessing phase, and the ''Sign'' algorithm can be run immediately when the messages and set of signers are determined. +This way, the final signature is created quicker and with fewer round trips. +However, applications that use this method presumably store the nonces for a longer time and must therefore be even more careful not to reuse them. + +In general, signers are stateful in the sense that they first generate ''secnonce'' and then need to store it until they receive the coordinator's response containing the aggregate nonce and session parameters. +However, it is possible for one of the signers to be stateless: this signer waits until it receives the ''pubnonce'' of all other signers and until session parameters are determined. +Then, the signer can run ''NonceGen'', ''NonceAgg'', and ''Sign'' in sequence and send out its ''pubnonce'' along with its partial signature. + +=== Role of the Coordinator === + +Instead of every signer sending their nonce and partial signature to every other signer, it is possible to use an untrusted ''coordinator'' to reduce the communication complexity from quadratic to linear in the number of signers. +In each of the two rounds, the coordinator collects all signers' contributions (nonces or partial signatures), processes them, and sends the results back to the signers. +A malicious coordinator can force the signing session to fail to produce a valid aggregate signature but cannot negatively affect the unforgeability of the scheme, i.e., even a malicious coordinator colluding with all but one signer cannot forge a signature. + +If there is no dedicated coordinator, one of the signers can act as the coordinator. + +Unlike BIP 327 where the coordinator only needs to send the aggregate nonce back to signers, the coordinator in this scheme must also send each signer the individual public nonces (specifically, the ''R2'' components) of all signers. +This is because each signer must verify that their own ''R2'' value appears exactly once in the session context (see [[#sign|Sign]]). +In many applications, the signers already know the list of public keys and messages before the signing session starts and only the ''aggnonce'' and individual public nonces need to be communicated. + +=== List Ordering === + +The aggregate signature depends on the order of the list of public key and message pairs. +A signature valid for a list ''L'' will not be valid for any non-trivial permutation of ''L'' (except with negligible probability). +If an application does not have a natural ordering of signers, the list should be sorted using a predetermined order before being provided to the coordinator, and the verifier must use the same order. + +== 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 ''cbytes(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns the 33-byte encoding consisting of the single byte ''0x02'' if ''has_even_y(P)'', or ''0x03'' otherwise, prepended to ''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'' and ''has_even_y(P)'', or fails if ''x ≥ p'' 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 ''cpoint(x)'', where ''x'' is a 33-byte array, sets ''P'' to the point on the curve whose X coordinate is ''int(x[1:33])''. Fail if ''x[0]'' is not ''0x02'' or ''0x03'', if ''int(x[1:33]) ≥ p'', or if no point with that X coordinate exists on the curve. If ''x[0] = 0x02'', return the point with even y; if ''x[0] = 0x03'', return the point with odd y. +** 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 [[bip-0459/reference.py|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)'' +|- +| ''cpoint(x)'' || ''GE.from_bytes_compressed(x)'' +|- +| ''int(x)'', failing if ''≥ n'' || ''Scalar.from_bytes_checked(x)'' +|- +| ''int(x) mod n'' || ''Scalar.from_bytes_wrapping(x)'' +|- +| ''bytes(P)'' || ''P.to_bytes_xonly()'' +|- +| ''cbytes(P)'' || ''P.to_bytes_compressed()'' +|- +| ''bytes(s)'' || ''s.to_bytes()'' +|- +| ''has_even_y(P)'' || ''P.has_even_y()'' +|- +| ''hashtag(x)'' || ''tagged_hash(tag, x)'' +|} + +=== Algorithms === + +The following specification of the algorithms has been written with a focus on clarity. +As a result, the specified algorithms are not always optimal in terms of computation and space. +In particular, some values are recomputed but can be cached in actual implementations (see [[#general-signing-flow|General Signing Flow]]). + +Unless noted otherwise, any of the ''pk0..u-1'', ''m0..u-1'', ''pubnonce0..u-1'', and ''psig0..u-1'' arrays that an algorithm takes have the same length ''u'', with ''u ≥ 1'', and the algorithm fails otherwise. +Each message is a 32-byte array, and an algorithm fails on any message of a different length. + +==== NonceGen ==== + +''NonceGen'' generates a secret nonce and a corresponding public nonce for a single signer. + +Input: +* ''sk'': a 32-byte secret key (optional argument) +* ''extra_in'': a byte array (optional argument) + +'''''NonceGen(sk, extra_in)'''''See [[#nonce-generation|Nonce Generation]] in the Overview for important security requirements regarding randomness and nonce reuse.: +* Let ''rand' '' be a 32-byte array freshly drawn uniformly at random. +* If the optional argument ''sk'' is present: +** Let ''rand'' be the byte-wise xor of ''sk'' and ''hashFullAgg/aux(rand')''. +* Else: +** Let ''rand = rand' ''. +* If the optional argument ''extra_in'' is not present: +** Let ''extra_in'' be an empty byte array. +* Let ''r1 = int(hashFullAgg/nonce(rand || extra_in || 0x00)) mod n''; fail if ''r1 = 0''. +* Let ''r2 = int(hashFullAgg/nonce(rand || extra_in || 0x01)) mod n''; fail if ''r2 = 0''. +* Let ''R1 = r1⋅G''. +* Let ''R2 = r2⋅G''. +* Let ''secnonce = bytes(r1) || bytes(r2)''. +* Let ''pubnonce = cbytes(R1) || cbytes(R2)''. +* Return ''(secnonce, pubnonce)''. + +==== NonceAgg ==== + +''NonceAgg'' aggregates all signers' public nonces into a single aggregate nonce. + +Input: +* ''pubnonce0..u-1'': an array of ''u'' 66-byte public nonces + +'''''NonceAgg(pubnonce0..u-1)''''': +* For ''i = 0 .. u-1'': +** Let ''R1,i = cpoint(pubnoncei[0:33])''; fail if that fails. +** Let ''R2,i = cpoint(pubnoncei[33:66])''; fail if that fails. +* Let ''R1 = R1,0 + R1,1 + ... + R1,u-1''. +* Let ''R2 = R2,0 + R2,1 + ... + R2,u-1''. +* Fail if ''is_infinite(R1)'' or ''is_infinite(R2)''. +* Let ''aggnonce = cbytes(R1) || cbytes(R2)''. +* Return ''aggnonce''. + +==== GetSessionValues ==== + +''GetSessionValues'' is a helper function that computes the derived values shared by ''Sign'', ''SigAgg'', and ''PartialSigVerify''. + +Input: +* ''aggnonce'': a 66-byte aggregate nonce +* ''pk0..u-1'': an array of ''u'' 32-byte public keys +* ''m0..u-1'': an array of ''u'' 32-byte messages +* ''pubnonce0..u-1'': an array of ''u'' 66-byte public nonces + +'''''GetSessionValues(aggnonce, pk0..u-1, m0..u-1, pubnonce0..u-1)''''': +* Let ''R1 = cpoint(aggnonce[0:33])''; fail if that fails. +* Let ''R2 = cpoint(aggnonce[33:66])''; fail if that fails. +* For ''i = 0 .. u-1'': +** Let ''R2,i = cpoint(pubnoncei[33:66])''; fail if that fails. +* Let ''nonce_data = cbytes(R1) || cbytes(R2)'' +* For ''i = 0 .. u-1'': +** Let ''nonce_data = nonce_data || pki || mi || cbytes(R2,i)'' +* Let ''b = int(hashFullAgg/noncecoef(nonce_data)) mod n'' +* Let ''R = R1 + b⋅R2'' +* Fail if ''is_infinite(R)''. +* Return ''(R, b)'' + +==== Sign ==== + +''Sign'' produces a partial signature for a signer. + +Input: +* ''secnonce'': a 64-byte secret nonce +* ''sk'': a 32-byte secret key +* ''m'': the 32-byte message this signer intends to sign +* ''aggnonce'': a 66-byte aggregate nonce +* ''pk0..u-1'': an array of ''u'' 32-byte public keys +* ''m0..u-1'': an array of ''u'' 32-byte messages +* ''pubnonce0..u-1'': an array of ''u'' 66-byte public nonces + +'''''Sign(secnonce, sk, m, aggnonce, pk0..u-1, m0..u-1, pubnonce0..u-1)''''': +* Let ''r1 = int(secnonce[0:32])''; fail if ''r1 = 0''. +* Let ''r2 = int(secnonce[32:64])''; fail if ''r2 = 0''. +* Fail if ''r1 ≥ n'' or ''r2 ≥ n''. +* Let ''d = int(sk)''; fail if ''d = 0'' or ''d ≥ n''. +* Let ''P = d⋅G''. +* Let ''R2 = r2⋅G''. +* '''Index lookup and uniqueness check''': Find the unique index ''j'' in ''0..u-1'' such that ''pubnoncej[33:66] = cbytes(R2)''. Fail if no such index exists or if more than one such index exists.The uniqueness check applies to the ''R2'' value alone. Checking instead that the combination of public key, message, and ''R2'' occurs exactly once is not sufficient. If the signer's ''R2'' appears at a second index with a different public key or message, ''Sign'' must fail, because signing such a session enables a key extraction attack (see Section 4.2 of the [https://eprint.iacr.org/2025/692 DahLIAS paper]). +* Fail if ''pkj ≠ bytes(P)'' or ''mj ≠ m'', i.e., the public key or message at the signer's position does not match the signer's secret key or intended message. +* Let ''(R, b) = GetSessionValues(aggnonce, pk0..u-1, m0..u-1, pubnonce0..u-1)''; fail if that fails. +* Let ''e = 1'' if ''has_even_y(R)'', otherwise let ''e = n - 1''. +* Let ''d' = d'' if ''has_even_y(P)'', otherwise let ''d' = n - d''. +* Let ''L = pk0 || m0 || ... || pku-1 || mu-1''. +* Let ''cj = int(hashFullAgg/sig(L || bytes(R) || pkj || mj)) mod n''. +* Let ''sj = (e⋅(r1 + b⋅r2) + cj⋅d') mod n''. +* Let ''psig = bytes(sj)''. +* Return ''psig''. + +==== SigAgg ==== + +''SigAgg'' aggregates all partial signatures into a final 64-byte aggregate signature. + +Input: +* ''aggnonce'': a 66-byte aggregate nonce +* ''pk0..u-1'': an array of ''u'' 32-byte public keys +* ''m0..u-1'': an array of ''u'' 32-byte messages +* ''pubnonce0..u-1'': an array of ''u'' 66-byte public nonces +* ''psig0..u-1'': an array of ''u'' 32-byte partial signatures + +'''''SigAgg(aggnonce, pk0..u-1, m0..u-1, pubnonce0..u-1, psig0..u-1)''''': +* Let ''(R, b) = GetSessionValues(aggnonce, pk0..u-1, m0..u-1, pubnonce0..u-1)''; fail if that fails. +* For ''i = 0 .. u-1'': +** Let ''si = int(psigi)''; fail if ''si ≥ n''. +* Let ''s = (s0 + s1 + ... + su-1) mod n''. +* Let ''sig = bytes(R) || bytes(s)''. +* Return ''sig''. + +==== PartialSigVerify ==== + +''PartialSigVerify'' verifies a single signer's partial signature and is used to identify disruptive signers. +It recomputes the aggregate nonce from the public nonces rather than taking it as an argument, so its result cannot be influenced by the coordinator. +If the public nonces are received over authenticated channels and ''PartialSigVerify'' succeeds for the partial signatures of all ''u'' signers, then ''SigAgg'' called with ''aggnonce = NonceAgg(pubnonce0..u-1)'' produces a valid aggregate signature. + +Input: +* ''psig'': a 32-byte partial signature +* ''pk0..u-1'': an array of ''u'' 32-byte public keys +* ''m0..u-1'': an array of ''u'' 32-byte messages +* ''pubnonce0..u-1'': an array of ''u'' 66-byte public nonces +* ''signer_index'': the index of the signer in the list (integer in ''0..u-1'') + +'''''PartialSigVerify(psig, pk0..u-1, m0..u-1, pubnonce0..u-1, signer_index)''''': +* Let ''i = signer_index''. +* Fail if ''i < 0'' or ''i ≥ u''. +* Let ''s = int(psig)''; fail if ''s ≥ n''. +* Let ''R1,i = cpoint(pubnoncei[0:33])''; fail if that fails. +* Let ''R2,i = cpoint(pubnoncei[33:66])''; fail if that fails. +* Let ''Pi = lift_x(int(pki))''; fail if that fails. +* Let ''aggnonce = NonceAgg(pubnonce0..u-1)''; fail if that fails. +* Let ''(R, b) = GetSessionValues(aggnonce, pk0..u-1, m0..u-1, pubnonce0..u-1)''; fail if that fails. +* Let ''e = 1'' if ''has_even_y(R)'', otherwise let ''e = n - 1''. +* Let ''L = pk0 || m0 || ... || pku-1 || mu-1''. +* Let ''ci = int(hashFullAgg/sig(L || bytes(R) || pki || mi)) mod n''. +* Let ''R'i = R1,i + b⋅R2,i''. +* Fail if ''s⋅G ≠ e⋅R'i + ci⋅Pi''. +* Return success iff no failure occurred before reaching this point. + +==== Verify ==== + +''Verify'' verifies a given 64-byte aggregate signature against an ordered list of public key and message pairs. +The inputs to ''Verify'' are the only data required by a verifier. +No nonces or other session data are needed. + +Input: +* ''pk0..u-1'': an array of ''u'' 32-byte public keys +* ''m0..u-1'': an array of ''u'' 32-byte messages +* ''sig'': a 64-byte aggregate signature + +'''''Verify(pk0..u-1, m0..u-1, sig)'''''An [https://bitcoin.stackexchange.com/a/80702/109853 efficient algorithm for computing a sum of multiple EC multiplications] can significantly speed up verification.: +* Let ''r = sig[0:32]''. +* Let ''s = int(sig[32:64])''; fail if ''s ≥ n''. +* Let ''R = lift_x(int(r))''; fail if that fails. +* For ''i = 0 .. u-1'': +** Let ''Pi = lift_x(int(pki))''; fail if that fails. +* Let ''L = pk0 || m0 || ... || pku-1 || mu-1''. +* For ''i = 0 .. u-1'': +** Let ''ci = int(hashFullAgg/sig(L || r || pki || mi)) mod n''. +* Fail if ''s⋅G ≠ R + c0⋅P0 + c1⋅P1 + ... + cu-1⋅Pu-1''. +* Return success iff no failure occurred before reaching this point. + +=== Key Tweaking === + +The scheme supports additive key tweaking, allowing signers to use tweaked key pairs without changes to the verification algorithm. +A tweak ''t'' is a 256-bit integer with ''0 ≤ t < 2256''. +In both variants below, tweaking fails if ''t ≥ n'' or if ''is_infinite(P')''. +The tweaked public key ''bytes(P')'' appears in the public key list provided to ''Verify'', and the signer uses ''sk' '' as its secret key in ''Sign''. + +'''Plain tweaking''' modifies a key pair ''(sk, P)'' with ''P = sk⋅G'' and is used for BIP 32 unhardened derivation, where ''t'' is the child offset: +* ''sk' = (sk + t) mod n'' +* ''P' = P + t⋅G'' + +'''X-only tweaking''' is used for BIP 341 Taproot key path spending. +Because an X-only public key denotes the point with even Y, the input key is first normalized to even Y. +Let ''d = sk'' if ''has_even_y(P)'', otherwise ''d = n - sk'', so that ''d⋅G = lift_x(x(P))''. +Then: +* ''sk' = (d + t) mod n'' +* ''P' = lift_x(x(P)) + t⋅G'' +For BIP 341, ''t = int(hashTapTweak(bytes(P) || merkle_root))'' where ''bytes(P)'' is the X-only internal public key. +The resulting ''P' '' may have odd Y. + +== Rationale == + +DahLIAS is the first aggregate signature scheme with constant-size signatures based directly on discrete logarithms in pairing-free groups. + +* A fully aggregated signature has the same shape as a standard Schnorr signature: a pair ''(R, s)'' serialized as 64 bytes. The verification algorithm is different from BIP 340 verification and takes a ''list'' of public key and message pairs as input rather than a single pair. +* Every message is exactly 32 bytes. This is the size of a transaction sighash, the intended input for cross-input signature aggregation. Fixing the length also keeps the encoding of the list ''L'' unambiguous, since the challenge binds to the entire list rather than to a single message as in BIP 340 and BIP 327. An application that needs to sign data of a different size should pre-hash it to a 32-byte digest, for example with a tagged hash. +* Each signer contributes two nonces rather than one. A single nonce per signer would leave the scheme open to the [https://eprint.iacr.org/2020/945 ROS attack] mounted across many concurrent signing sessions. Deriving the common nonce as ''R = R1 + b⋅R2'', with ''b'' bound to the full session context, prevents this, following MuSig2 ([[bip-0327.mediawiki|BIP 327]]). A variant of the attack that exploits a signer signing multiple messages is in turn prevented by the ''R2'' uniqueness and message checks in ''Sign'' (see [[#security|Security]]). +* A single nonce coefficient ''b'' is applied uniformly to all signers' second nonce contributions. This keeps signing cost constant per signer (three group exponentiations) rather than requiring a multi-exponentiation of size ''u''. The trade-off is the need for a uniqueness check on the second nonce component ''R2'' during signing. +* The scheme supports additive key tweaking, enabling compatibility with BIP 32 key derivation and BIP 341 Taproot outputs. +* This document does not specify an optimized signing algorithm for a single party that knows all secret keys, e.g., a wallet spending several of its own inputs. Such a party can act as all signers and the coordinator and run the protocol locally. A more efficient method [https://groups.google.com/g/bitcoindev/c/eothFkxAvK0/m/PgRaFPovEQAJ has been outlined] in the mailing list discussion but requires additional security analysis before it can be specified. + +A non-interactive alternative is half-aggregation ([[bip-0458.mediawiki|BIP 458]]), which requires no cooperation between the signers but compresses ''n'' signatures to ''32*n + 32'' bytes rather than to a constant 64 bytes. + +== Security == + +'''Nonce generation and reuse.''' As detailed in [[#nonce-generation|Nonce Generation]] and [[#general-signing-flow|General Signing Flow]], ''NonceGen'' must draw ''rand' '' from a high-quality random number generator, nonces must not be derived deterministically, and a ''secnonce'' must never be used in more than one ''Sign'' invocation. +Violating any of these can allow an adversary to extract the signer's secret key. + +'''Signer verification checks.''' The checks performed by ''Sign'' after recomputing ''R2'' are security-critical. +The signer locates its position by finding the unique index ''j'' whose ''R2'' matches its own. +These ''R2'' values act as ephemeral identifiers that distinguish signers even when they share a public key, and omitting the uniqueness check enables key extraction. +The signer must also confirm that ''both'' the public key and the message at index ''j'' match its own. +Verifying only the public key would let a malicious coordinator substitute a different message at the signer's position. + +'''Partial signatures.''' A partial signature is ''not'' a signature. +By choosing its nonces accordingly, an adversary can produce a partial signature that verifies for any public key. +''PartialSigVerify'' therefore serves only to identify disruptive signers and does not prove that a signer knows the corresponding secret key. + +'''Untrusted coordinator.''' The coordinator can cause a signing session to abort but cannot forge a signature, even when colluding with all but one signer (see [[#role-of-the-coordinator|Role of the Coordinator]]). + +'''Security model.''' The scheme is proven secure in the concurrent setting, including when signers use key tweaking, under the algebraic one-more discrete logarithm (AOMDL) assumption, the same assumption used for [[bip-0327.mediawiki|BIP 327]].The precise notion is cosigners-aware existential unforgeability (co-EUF-CMA) as defined in the DahLIAS paper. Each signer sees the entire list of public key and message pairs before finalizing its contribution, and unforgeability guarantees that no valid aggregate signature can attribute to an honest signer a message and cosigner list it never agreed to sign. This all-or-nothing guarantee over the whole list distinguishes interactive aggregate signatures from multisignatures like MuSig2, which produce a signature for a single message under a single aggregate key. +AOMDL is a falsifiable and weaker variant of the well-studied OMDL problem. +The proof models ''hashFullAgg/sig'' as a random oracle and requires only that ''hashFullAgg/noncecoef'' be collision-resistant. +The scheme is also strongly binding, meaning it is infeasible to produce a single signature valid for two distinct lists of public key and message pairs.DahLIAS strong binding additionally requires the verifier to reject any public key equal to the point at infinity. This condition is satisfied implicitly in this specification: public keys are 32-byte X-only values that are decoded with ''lift_x'', which never yields the point at infinity, so no such public key can appear in the list. + +== Backward Compatibility == + +Full-aggregation specifies a new, optional interactive signing protocol for producing an aggregate signature for a list of BIP 340 public key and message pairs. +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. + +== Test Vectors and Reference Code == + +A reference implementation in Python is provided in [[bip-0459/reference.py|reference.py]]. +Note that the reference implementation depends on a vendored copy of the [https://github.com/secp256k1lab/secp256k1lab secp256k1lab library]. + +Test vectors are provided in [[bip-0459/vectors/|vectors]] and are generated with a [[bip-0459/gen_test_vectors.py|generator script]]. +The reference implementation can be executed with the test vectors by running [[bip-0459/run_test_vectors.py|the run_test_vectors script]]. + +== Acknowledgements == + +The scheme specified in this document is DahLIAS (Discrete Logarithm-Based Interactive Aggregate Signatures) by Jonas Nick, Tim Ruffing, and Yannick Seurin. +Thanks to waxwing for his feedback on the mailing list, which, among other things, led to clarifying the uniqueness check in ''Sign'' and adding test vectors for it. + +== Footnotes == + + diff --git a/bip-0459/gen_test_vectors.py b/bip-0459/gen_test_vectors.py new file mode 100644 index 0000000000..815e2b9adc --- /dev/null +++ b/bip-0459/gen_test_vectors.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +""" +Generate test vectors for signature aggregation of BIP 340 signatures +per the DahLIAS interactive signing protocol. + +Usage: + python gen_test_vectors.py + +Outputs: + - vectors/test_vectors_sign.csv (end-to-end signing with fixed nonces) + - vectors/test_vectors_sign_error.csv (sessions in which Sign must fail) + - vectors/test_vectors_verify.csv (Verify: success and failure cases) + - vectors/test_vectors_partial_sig_verify.csv (PartialSigVerify cases) + - vectors/test_vectors_tweak.csv (plain and X-only key tweaking) +""" + +import csv +import os +import sys +from pathlib import Path +from typing import List + +sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src")) +from secp256k1lab.secp256k1 import G, GE, Scalar +from secp256k1lab.util import bytes_from_int, int_from_bytes + +from reference import ( + NonceAgg, + Sign, + SigAgg, + Verify, + PartialSigVerify, + TweakSK, + TweakPK, + has_even_y, + n, +) + + +VECTORS_DIR = Path(__file__).resolve().parent / "vectors" +SIGN_VECTORS = VECTORS_DIR / "test_vectors_sign.csv" +SIGN_ERROR_VECTORS = VECTORS_DIR / "test_vectors_sign_error.csv" +VERIFY_VECTORS = VECTORS_DIR / "test_vectors_verify.csv" +PARTIAL_SIG_VERIFY_VECTORS = VECTORS_DIR / "test_vectors_partial_sig_verify.csv" +TWEAK_VECTORS = VECTORS_DIR / "test_vectors_tweak.csv" + + +def scalar_from_byte(b: int) -> Scalar: + """Scalar whose 32-byte big-endian encoding is bytes([b]*32). Safe for 0x01..0xee.""" + assert 1 <= b <= 0xee + return Scalar.from_bytes_checked(bytes([b] * 32)) + + +def make_signer(i: int): + """Deterministic (sk, pk, msg, secnonce, pubnonce). sk is normalized so pk has even y.""" + sk = scalar_from_byte(i * 3 + 1) + pk = sk * G + if not has_even_y(pk): + sk = -sk + pk = sk * G + r1 = scalar_from_byte(i * 3 + 2) + r2 = scalar_from_byte(i * 3 + 3) + R1, R2 = r1 * G, r2 * G + msg = bytes([0x80 + i] * 32) + return sk, pk, msg, (r1, r2), (R1, R2) + + +def make_same_pk_signers(): + """Two signers sharing one key, signing different messages.""" + sk = scalar_from_byte(1) + pk = sk * G + if not has_even_y(pk): + sk = -sk + pk = sk * G + msg_a = bytes([0xA0] * 32) + msg_b = bytes([0xA1] * 32) + r1_a, r2_a = scalar_from_byte(11), scalar_from_byte(12) + r1_b, r2_b = scalar_from_byte(13), scalar_from_byte(14) + return [ + (sk, pk, msg_a, (r1_a, r2_a), (r1_a * G, r2_a * G)), + (sk, pk, msg_b, (r1_b, r2_b), (r1_b * G, r2_b * G)), + ] + + +def make_tweaked_signer(i: int, tweak_byte: int, is_xonly: bool): + """Signer with a tweaked key pair, using an odd-y base for X-only tweaks to exercise normalization.""" + sk, pk, msg, secnonce, pubnonce = make_signer(i) + if is_xonly: + sk, pk = -sk, -pk + t = scalar_from_byte(tweak_byte) + return TweakSK(sk, t, is_xonly), TweakPK(pk, t, is_xonly), msg, secnonce, pubnonce + + +def make_duplicate_pm_signers(): + """Two signers with identical (pk, m) but distinct nonces.""" + sk = scalar_from_byte(1) + pk = sk * G + if not has_even_y(pk): + sk = -sk + pk = sk * G + msg = bytes([0xC0] * 32) + r1_a, r2_a = scalar_from_byte(20), scalar_from_byte(21) + r1_b, r2_b = scalar_from_byte(22), scalar_from_byte(23) + return [ + (sk, pk, msg, (r1_a, r2_a), (r1_a * G, r2_a * G)), + (sk, pk, msg, (r1_b, r2_b), (r1_b * G, r2_b * G)), + ] + + +def serialize_pubnonce(pubnonce) -> bytes: + R1, R2 = pubnonce + return R1.to_bytes_compressed() + R2.to_bytes_compressed() + + +def serialize_secnonce(secnonce) -> bytes: + r1, r2 = secnonce + return r1.to_bytes() + r2.to_bytes() + + +def serialize_sig(sig) -> bytes: + R, s = sig + return R.to_bytes_xonly() + s.to_bytes() + + +def serialize_pk(pk: GE) -> bytes: + return pk.to_bytes_xonly() + + +def format_list(items: List[bytes], sep: str = ";") -> str: + if not items: + return "" + return sep.join(item.hex().upper() for item in items) + + +def sign_session(signers): + """Run NonceAgg -> Sign (per signer) -> SigAgg with deterministic inputs.""" + sks = [s[0] for s in signers] + pks = [GE.from_bytes_xonly(s[1].to_bytes_xonly()) for s in signers] + msgs = [s[2] for s in signers] + secnonces = [s[3] for s in signers] + pubnonces = [s[4] for s in signers] + aggnonce = NonceAgg(pubnonces) + psigs = [ + Sign(secnonces[i], sks[i], msgs[i], aggnonce, pks, msgs, pubnonces) + for i in range(len(sks)) + ] + sig = SigAgg(aggnonce, pks, msgs, pubnonces, psigs) + return aggnonce, psigs, sig + + +def gen_sign_vectors(f): + writer = csv.writer(f) + writer.writerow(( + "index", + "secret_keys", + "messages", + "secnonces", + "expected_aggnonce", + "expected_partial_sigs", + "expected_sig", + "comment", + )) + + def write_row(idx, signers, comment): + aggnonce, psigs, sig = sign_session(signers) + writer.writerow(( + idx, + format_list([s[0].to_bytes() for s in signers]), + format_list([s[2] for s in signers]), + format_list([serialize_secnonce(s[3]) for s in signers]), + serialize_pubnonce(aggnonce).hex().upper(), + format_list([p.to_bytes() for p in psigs]), + serialize_sig(sig).hex().upper(), + comment, + )) + pks = [s[1] for s in signers] + msgs = [s[2] for s in signers] + pubnonces = [s[4] for s in signers] + for i in range(len(signers)): + assert PartialSigVerify(psigs[i], pks, msgs, pubnonces, i), \ + f"generated psig failed self-verification: {comment}" + assert Verify(pks, msgs, sig), \ + f"generated sig failed self-verification: {comment}" + + write_row(0, [make_signer(0)], "Single signer") + write_row(1, [make_signer(i) for i in range(2)], "Two signers") + write_row(2, [make_signer(i) for i in range(3)], "Three signers") + write_row(3, [make_signer(i) for i in range(5)], "Five signers") + write_row(4, make_same_pk_signers(), "Same pubkey signs two different messages") + write_row(5, [make_tweaked_signer(0, 0x21, False), make_tweaked_signer(1, 0x22, True)], + "Signers with tweaked keys") + write_row(6, make_duplicate_pm_signers(), "Exact duplicate (pk, m) entries") + + +def gen_sign_error_vectors(f): + writer = csv.writer(f) + writer.writerow(( + "index", "secret_key", "message", "secnonce", + "pubkeys", "messages", "pubnonces", + "comment", + )) + + def write_row(idx, signer, pks, msgs, pubnonces, comment): + sk, _, msg, secnonce, _ = signer + aggnonce = NonceAgg(pubnonces) + try: + Sign(secnonce, sk, msg, aggnonce, pks, msgs, pubnonces) + except (AssertionError, ValueError): + pass + else: + raise RuntimeError(f"Sign unexpectedly succeeded: {comment}") + writer.writerow(( + idx, + sk.to_bytes().hex().upper(), + msg.hex().upper(), + serialize_secnonce(secnonce).hex().upper(), + format_list([serialize_pk(p) for p in pks]), + format_list(msgs), + format_list([serialize_pubnonce(p) for p in pubnonces]), + comment, + )) + + signers = [make_signer(i) for i in range(3)] + pks = [GE.from_bytes_xonly(s[1].to_bytes_xonly()) for s in signers] + msgs = [s[2] for s in signers] + pubnonces = [s[4] for s in signers] + + dup_pubnonces = list(pubnonces) + dup_pubnonces[2] = (pubnonces[2][0], pubnonces[0][1]) + write_row(0, signers[0], pks, msgs, dup_pubnonces, + "Signer's R2 appears at two indices") + + missing_pubnonces = list(pubnonces) + missing_pubnonces[0] = (pubnonces[0][0], scalar_from_byte(0x63) * G) + write_row(1, signers[0], pks, msgs, missing_pubnonces, + "Signer's R2 appears at no index") + + wrong_msgs = list(msgs) + wrong_msgs[0] = bytes([0xEE] * 32) + write_row(2, signers[0], pks, wrong_msgs, pubnonces, + "Message at signer's index was substituted") + + wrong_pks = list(pks) + wrong_pks[0] = GE.from_bytes_xonly((scalar_from_byte(0x55) * G).to_bytes_xonly()) + write_row(3, signers[0], wrong_pks, msgs, pubnonces, + "Public key at signer's index was substituted") + + +def gen_verify_vectors(f): + writer = csv.writer(f) + writer.writerow(( + "index", "pubkeys", "messages", "sig", "expected_result", "comment", + )) + + idx = 0 + + signers = [make_signer(i) for i in range(2)] + _, _, sig = sign_session(signers) + pks = [serialize_pk(s[1]) for s in signers] + msgs = [s[2] for s in signers] + sig_bytes = serialize_sig(sig) + + writer.writerow((idx, format_list(pks), format_list(msgs), + sig_bytes.hex().upper(), "TRUE", + "Two-signer valid aggregate")) + idx += 1 + + signers3 = [make_signer(i) for i in range(3)] + _, _, sig3 = sign_session(signers3) + pks3 = [serialize_pk(s[1]) for s in signers3] + msgs3 = [s[2] for s in signers3] + sig3_bytes = serialize_sig(sig3) + writer.writerow((idx, format_list(pks3), format_list(msgs3), + sig3_bytes.hex().upper(), "TRUE", + "Three-signer valid aggregate")) + idx += 1 + + wrong_msgs = [msgs[0], bytes([0xff] * 32)] + writer.writerow((idx, format_list(pks), format_list(wrong_msgs), + sig_bytes.hex().upper(), "FALSE", + "Wrong message for second signer")) + idx += 1 + + writer.writerow((idx, format_list([pks[1], pks[0]]), format_list(msgs), + sig_bytes.hex().upper(), "FALSE", + "Pubkey order swapped")) + idx += 1 + + writer.writerow((idx, format_list([pks[0]]), format_list([msgs[0]]), + sig_bytes.hex().upper(), "FALSE", + "Dropped second signer")) + idx += 1 + + s_int = int_from_bytes(sig_bytes[32:64]) + tampered = sig_bytes[:32] + bytes_from_int(s_int ^ 1) + writer.writerow((idx, format_list(pks), format_list(msgs), + tampered.hex().upper(), "FALSE", + "Tampered s (bit flip)")) + idx += 1 + + s_zero = sig_bytes[:32] + bytes(32) + writer.writerow((idx, format_list(pks), format_list(msgs), + s_zero.hex().upper(), "FALSE", + "s = 0 (in range, wrong)")) + idx += 1 + + s_eq_n = sig_bytes[:32] + bytes_from_int(n) + writer.writerow((idx, format_list(pks), format_list(msgs), + s_eq_n.hex().upper(), "FALSE", + "s = n (out of range)")) + idx += 1 + + s_max = sig_bytes[:32] + bytes([0xff] * 32) + writer.writerow((idx, format_list(pks), format_list(msgs), + s_max.hex().upper(), "FALSE", + "s = 2^256 - 1")) + idx += 1 + + def find_off_curve_x() -> bytes: + for i in range(1, 256): + cand = bytes([i] * 32) + try: + GE.from_bytes_xonly(cand) + except Exception: + return cand + raise RuntimeError("could not find an off-curve x") + + off_curve = find_off_curve_x() + + bad_R_sig = off_curve + sig_bytes[32:64] + writer.writerow((idx, format_list(pks), format_list(msgs), + bad_R_sig.hex().upper(), "FALSE", + "R x-coord not on curve")) + idx += 1 + + writer.writerow((idx, format_list([off_curve, pks[1]]), format_list(msgs), + sig_bytes.hex().upper(), "FALSE", + "Public key not on curve")) + idx += 1 + + writer.writerow((idx, format_list(pks), format_list(msgs), + sig_bytes[:-1].hex().upper(), "FALSE", + "Signature too short")) + idx += 1 + + writer.writerow((idx, format_list(pks), format_list(msgs), + (sig_bytes + b"\x00").hex().upper(), "FALSE", + "Signature too long")) + idx += 1 + + writer.writerow((idx, format_list(pks), format_list([msgs[0]]), + sig_bytes.hex().upper(), "FALSE", + "Pubkey/message count mismatch")) + idx += 1 + + writer.writerow((idx, "", "", + sig_bytes.hex().upper(), "FALSE", + "Empty signer list")) + idx += 1 + + +def gen_partial_sig_verify_vectors(f): + writer = csv.writer(f) + writer.writerow(( + "index", "signer_index", "psig", + "pubkeys", "messages", "pubnonces", + "expected_result", "comment", + )) + + signers = [make_signer(i) for i in range(2)] + aggnonce, psigs, _ = sign_session(signers) + pks_hex = format_list([serialize_pk(s[1]) for s in signers]) + msgs_hex = format_list([s[2] for s in signers]) + pubnonces_hex = format_list([serialize_pubnonce(s[4]) for s in signers]) + + def write_row(idx, signer_index, psig_bytes, expected, comment): + writer.writerow(( + idx, signer_index, psig_bytes.hex().upper(), + pks_hex, msgs_hex, pubnonces_hex, + expected, comment, + )) + + idx = 0 + write_row(idx, 0, psigs[0].to_bytes(), "TRUE", "Honest partial sig (signer 0)") + idx += 1 + write_row(idx, 1, psigs[1].to_bytes(), "TRUE", "Honest partial sig (signer 1)") + idx += 1 + write_row(idx, 1, psigs[0].to_bytes(), "FALSE", + "Honest psig verified at the wrong signer_index") + idx += 1 + write_row(idx, 0, bytes_from_int(int(psigs[0]) ^ 1), "FALSE", + "Tampered partial sig (bit flip)") + idx += 1 + write_row(idx, 0, bytes(32), "FALSE", "psig = 0 (in range, wrong)") + idx += 1 + write_row(idx, 0, bytes_from_int(n), "FALSE", "psig = n (out of range)") + idx += 1 + write_row(idx, 2, psigs[0].to_bytes(), "FALSE", "signer_index out of range") + idx += 1 + fake_aggnonce = (aggnonce[0] + G, aggnonce[1]) + psig_wrong = Sign(signers[0][3], signers[0][0], signers[0][2], fake_aggnonce, + [s[1] for s in signers], [s[2] for s in signers], + [s[4] for s in signers]) + write_row(idx, 0, psig_wrong.to_bytes(), "FALSE", "psig for a different aggregate nonce") + idx += 1 + R1_0, R2_0 = signers[0][4] + _, R2_1 = signers[1][4] + inf_pubnonces = format_list([serialize_pubnonce((R1_0, R2_0)), + serialize_pubnonce((-R1_0, R2_1))]) + writer.writerow((idx, 0, psigs[0].to_bytes().hex().upper(), + pks_hex, msgs_hex, inf_pubnonces, + "FALSE", "Aggregate nonce R1 is infinity")) + + +def gen_tweak_vectors(f): + writer = csv.writer(f) + writer.writerow(( + "index", "internal_secret_key", "tweak", "is_xonly", + "expected_secret_key", "expected_pubkey", "comment", + )) + + def row(idx, sk, t, is_xonly, comment): + sk_out = TweakSK(sk, t, is_xonly) + pk_out = TweakPK(sk * G, t, is_xonly) + assert sk_out * G == pk_out + writer.writerow(( + idx, sk.to_bytes().hex().upper(), t.to_bytes().hex().upper(), + "TRUE" if is_xonly else "FALSE", + sk_out.to_bytes().hex().upper(), pk_out.to_bytes_compressed().hex().upper(), + comment, + )) + + sk1 = scalar_from_byte(1) + even = sk1 if has_even_y(sk1 * G) else -sk1 + odd = -even + t = scalar_from_byte(0x02) + row(0, even, t, False, "Plain tweak of an even-y key") + row(1, odd, t, False, "Plain tweak of an odd-y key") + row(2, even, t, True, "X-only tweak of an even-y key") + row(3, odd, t, True, "X-only tweak of an odd-y key") + + +if __name__ == "__main__": + os.makedirs(VECTORS_DIR, exist_ok=True) + + print(f"Generating {SIGN_VECTORS}") + with open(SIGN_VECTORS, "w", newline="", encoding="utf-8") as f: + gen_sign_vectors(f) + + print(f"Generating {SIGN_ERROR_VECTORS}") + with open(SIGN_ERROR_VECTORS, "w", newline="", encoding="utf-8") as f: + gen_sign_error_vectors(f) + + print(f"Generating {VERIFY_VECTORS}") + with open(VERIFY_VECTORS, "w", newline="", encoding="utf-8") as f: + gen_verify_vectors(f) + + print(f"Generating {PARTIAL_SIG_VERIFY_VECTORS}") + with open(PARTIAL_SIG_VERIFY_VECTORS, "w", newline="", encoding="utf-8") as f: + gen_partial_sig_verify_vectors(f) + + print(f"Generating {TWEAK_VECTORS}") + with open(TWEAK_VECTORS, "w", newline="", encoding="utf-8") as f: + gen_tweak_vectors(f) + + print("Done.") diff --git a/bip-0459/reference.py b/bip-0459/reference.py new file mode 100644 index 0000000000..dc6aa0705d --- /dev/null +++ b/bip-0459/reference.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +Reference implementation of Full Aggregation of BIP 340 Signatures +per the DahLIAS interactive signing protocol. + +WARNING: This implementation is for demonstration purposes only and is not +optimized for production use. +""" + +from pathlib import Path +from typing import List, Tuple, Optional +import secrets +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, xor_bytes + +n = GE.ORDER + +FULLAGG_TAG_AUX = "FullAgg/aux" +FULLAGG_TAG_NONCE = "FullAgg/nonce" +FULLAGG_TAG_NONCECOEF = "FullAgg/noncecoef" +FULLAGG_TAG_SIG = "FullAgg/sig" + + +# +# Helpers +# + +def has_even_y(P: GE) -> bool: + return P.has_even_y() + + +def cbytes(P: GE) -> bytes: + return P.to_bytes_compressed() + + +def xbytes(P: GE) -> bytes: + return P.to_bytes_xonly() + + +# +# Key Tweaking +# + +def TweakSK(sk: Scalar, t: Scalar, is_xonly: bool) -> Scalar: + d = sk if (not is_xonly or has_even_y(sk * G)) else -sk + return d + t + + +def TweakPK(pk: GE, t: Scalar, is_xonly: bool) -> GE: + P = pk if (not is_xonly or has_even_y(pk)) else -pk + Q = P + t * G + assert not Q.infinity + return Q + + +# +# Nonce Generation and Aggregation +# + +def NonceGen(sk: Optional[Scalar] = None, + extra_in: bytes = b'') -> Tuple[Tuple[Scalar, Scalar], Tuple[GE, GE]]: + rand_prime = secrets.token_bytes(32) + if sk is not None: + rand = xor_bytes(sk.to_bytes(), tagged_hash(FULLAGG_TAG_AUX, rand_prime)) + else: + rand = rand_prime + r1 = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCE, rand + extra_in + b'\x00')) + r2 = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCE, rand + extra_in + b'\x01')) + assert r1 != 0 and r2 != 0 + R1, R2 = r1 * G, r2 * G + return (r1, r2), (R1, R2) + + +def NonceAgg(pubnonces: List[Tuple[GE, GE]]) -> Tuple[GE, GE]: + u = len(pubnonces) + R1, R2 = pubnonces[0] + for i in range(1, u): + R1 = R1 + pubnonces[i][0] + R2 = R2 + pubnonces[i][1] + if R1.infinity or R2.infinity: + raise ValueError("aggregate nonce is the point at infinity") + return R1, R2 + + +# +# Session Values +# + +def GetSessionValues(aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes], + pubnonces: List[Tuple[GE, GE]]) -> Tuple[GE, Scalar]: + R1, R2 = aggnonce + u = len(pks) + nonce_data = cbytes(R1) + cbytes(R2) + for i in range(u): + nonce_data += xbytes(pks[i]) + msgs[i] + cbytes(pubnonces[i][1]) + b = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCECOEF, nonce_data)) + R = R1 + b * R2 + assert not R.infinity + return R, b + + +# +# Signing +# + +def Sign(secnonce: Tuple[Scalar, Scalar], sk: Scalar, m: bytes, + aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes], + pubnonces: List[Tuple[GE, GE]]) -> Scalar: + assert len(pks) == len(msgs) == len(pubnonces) >= 1 + assert len(m) == 32 and all(len(mi) == 32 for mi in msgs) + r1, r2 = secnonce + assert r1 != 0 and r2 != 0 + assert sk != 0 + P = sk * G + R2_local = r2 * G + + # Index lookup and uniqueness check + matches = [j for j in range(len(pubnonces)) if pubnonces[j][1] == R2_local] + assert len(matches) == 1 + j = matches[0] + assert xbytes(pks[j]) == xbytes(P) and msgs[j] == m + + R, b = GetSessionValues(aggnonce, pks, msgs, pubnonces) + e = Scalar(1) if has_even_y(R) else -Scalar(1) + d_prime = sk if has_even_y(P) else -sk + + L = b'' + for i in range(len(pks)): + L += xbytes(pks[i]) + msgs[i] + c_j = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[j]) + msgs[j])) + s_j = e * (r1 + b * r2) + c_j * d_prime + return s_j + + +# +# Aggregation +# + +def SigAgg(aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes], + pubnonces: List[Tuple[GE, GE]], psigs: List[Scalar]) -> Tuple[GE, Scalar]: + assert len(pks) == len(msgs) == len(pubnonces) == len(psigs) >= 1 + assert all(len(mi) == 32 for mi in msgs) + R, _ = GetSessionValues(aggnonce, pks, msgs, pubnonces) + s = Scalar.sum(*psigs) + return R, s + + +# +# Partial Signature Verification +# + +def PartialSigVerify(psig: Scalar, pks: List[GE], msgs: List[bytes], + pubnonces: List[Tuple[GE, GE]], signer_index: int) -> bool: + if not (len(pks) == len(msgs) == len(pubnonces) >= 1) or any(len(mi) != 32 for mi in msgs): + return False + if not (0 <= signer_index < len(pks)): + return False + i = signer_index + R1_i, R2_i = pubnonces[i] + P_i = GE.from_bytes_xonly(xbytes(pks[i])) + R, b = GetSessionValues(NonceAgg(pubnonces), pks, msgs, pubnonces) + e = Scalar(1) if has_even_y(R) else -Scalar(1) + + L = b'' + for k in range(len(pks)): + L += xbytes(pks[k]) + msgs[k] + c_i = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[i]) + msgs[i])) + R_eff_i = R1_i + b * R2_i + return psig * G == e * R_eff_i + c_i * P_i + + +# +# Aggregate Signature Verification +# + +def Verify(pks: List[GE], msgs: List[bytes], sig: Tuple[GE, Scalar]) -> bool: + R_point, s = sig + if not (len(pks) == len(msgs) >= 1) or any(len(mi) != 32 for mi in msgs): + return False + u = len(pks) + R = GE.from_bytes_xonly(xbytes(R_point)) + Ps = [GE.from_bytes_xonly(xbytes(pks[i])) for i in range(u)] + + L = b'' + for i in range(u): + L += xbytes(pks[i]) + msgs[i] + rhs = R + for i in range(u): + c_i = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[i]) + msgs[i])) + rhs = rhs + c_i * Ps[i] + return s * G == rhs diff --git a/bip-0459/run_test_vectors.py b/bip-0459/run_test_vectors.py new file mode 100644 index 0000000000..321e12ee1b --- /dev/null +++ b/bip-0459/run_test_vectors.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +""" +Run test vectors for signature aggregation of BIP 340 signatures +per the DahLIAS interactive signing protocol. + +Usage: + python run_test_vectors.py + +Expects CSV files generated by gen_test_vectors.py in vectors/ directory. +""" + +import csv +import os +import sys +from pathlib import Path +from typing import List, Tuple + +sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src")) +from secp256k1lab.secp256k1 import G, GE, Scalar + +from reference import NonceAgg, Sign, SigAgg, Verify, PartialSigVerify, TweakSK, TweakPK + + +VECTORS_DIR = Path(__file__).resolve().parent / "vectors" +SIGN_VECTORS = VECTORS_DIR / "test_vectors_sign.csv" +SIGN_ERROR_VECTORS = VECTORS_DIR / "test_vectors_sign_error.csv" +VERIFY_VECTORS = VECTORS_DIR / "test_vectors_verify.csv" +PARTIAL_SIG_VERIFY_VECTORS = VECTORS_DIR / "test_vectors_partial_sig_verify.csv" +TWEAK_VECTORS = VECTORS_DIR / "test_vectors_tweak.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_pubnonce(b: bytes) -> Tuple[GE, GE]: + if len(b) != 66: + raise ValueError("pubnonce must be 66 bytes") + return GE.from_bytes_compressed(b[:33]), GE.from_bytes_compressed(b[33:]) + + +def parse_secnonce(b: bytes) -> Tuple[Scalar, Scalar]: + if len(b) != 64: + raise ValueError("secnonce must be 64 bytes") + return Scalar.from_bytes_checked(b[:32]), Scalar.from_bytes_checked(b[32:]) + + +def parse_sig(b: bytes) -> Tuple[GE, Scalar]: + """Parse R (32-byte x-only) || s (32-byte big-endian). + + Lifts R to even y and raises ValueError on length mismatch, off-curve R, or + s >= n. + """ + if len(b) != 64: + raise ValueError("signature must be 64 bytes") + R = GE.from_bytes_xonly(b[:32]) + s = Scalar.from_bytes_checked(b[32:]) + return R, s + + +def run_sign_tests() -> List[str]: + failures = [] + + with open(SIGN_VECTORS, newline="", encoding="utf-8") as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + (index, sks_str, msgs_str, secnonces_str, + expected_aggnonce_str, expected_psigs_str, + expected_sig_str, comment) = row + + try: + sks = [Scalar.from_bytes_checked(b) for b in parse_hex_list(sks_str)] + msgs = parse_hex_list(msgs_str) + secnonces = [parse_secnonce(b) for b in parse_hex_list(secnonces_str)] + expected_aggnonce = bytes.fromhex(expected_aggnonce_str) + expected_psigs = parse_hex_list(expected_psigs_str) + expected_sig = bytes.fromhex(expected_sig_str) + + pks = [GE.from_bytes_xonly((sk * G).to_bytes_xonly()) for sk in sks] + pubnonces = [(r1 * G, r2 * G) for (r1, r2) in secnonces] + + aggnonce = NonceAgg(pubnonces) + aggnonce_bytes = (aggnonce[0].to_bytes_compressed() + + aggnonce[1].to_bytes_compressed()) + if aggnonce_bytes != expected_aggnonce: + failures.append(f"Sign #{index}: {comment} (aggnonce mismatch)") + continue + + psigs = [ + Sign(secnonces[i], sks[i], msgs[i], aggnonce, pks, msgs, pubnonces) + for i in range(len(sks)) + ] + derived_psigs = [p.to_bytes() for p in psigs] + if derived_psigs != expected_psigs: + failures.append(f"Sign #{index}: {comment} (partial sig mismatch)") + continue + + for i in range(len(sks)): + if not PartialSigVerify(psigs[i], pks, msgs, pubnonces, i): + failures.append(f"Sign #{index}: {comment} (PartialSigVerify failed for signer {i})") + break + else: + R, s = SigAgg(aggnonce, pks, msgs, pubnonces, psigs) + actual_sig = R.to_bytes_xonly() + s.to_bytes() + if actual_sig != expected_sig: + failures.append(f"Sign #{index}: {comment} (aggsig mismatch)") + elif not Verify(pks, msgs, (R, s)): + failures.append(f"Sign #{index}: {comment} (Verify failed)") + except ValueError as e: + failures.append(f"Sign #{index}: {comment} (raised {e!r})") + + return failures + + +def run_sign_error_tests() -> List[str]: + failures = [] + + with open(SIGN_ERROR_VECTORS, newline="", encoding="utf-8") as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + (index, sk_str, msg_str, secnonce_str, + pks_str, msgs_str, pubnonces_str, comment) = row + + sk = Scalar.from_bytes_checked(bytes.fromhex(sk_str)) + m = bytes.fromhex(msg_str) + secnonce = parse_secnonce(bytes.fromhex(secnonce_str)) + pks = [GE.from_bytes_xonly(b) for b in parse_hex_list(pks_str)] + msgs = parse_hex_list(msgs_str) + pubnonces = [parse_pubnonce(b) for b in parse_hex_list(pubnonces_str)] + + try: + aggnonce = NonceAgg(pubnonces) + Sign(secnonce, sk, m, aggnonce, pks, msgs, pubnonces) + except (AssertionError, ValueError): + continue + failures.append(f"SignError #{index}: {comment} (Sign did not fail)") + + return failures + + +def run_partial_sig_verify_tests() -> List[str]: + failures = [] + + with open(PARTIAL_SIG_VERIFY_VECTORS, newline="", encoding="utf-8") as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + (index, signer_index_str, psig_str, + pks_str, msgs_str, pubnonces_str, + expected_result, comment) = row + expected = (expected_result == "TRUE") + + try: + signer_index = int(signer_index_str) + psig = Scalar.from_bytes_checked(bytes.fromhex(psig_str)) + pks = [GE.from_bytes_xonly(b) for b in parse_hex_list(pks_str)] + msgs = parse_hex_list(msgs_str) + pubnonces = [parse_pubnonce(b) for b in parse_hex_list(pubnonces_str)] + result = PartialSigVerify(psig, pks, msgs, pubnonces, signer_index) + except ValueError: + result = False + + if result != expected: + failures.append(f"PartialSigVerify #{index}: {comment}") + + return failures + + +def run_verify_tests() -> List[str]: + failures = [] + + with open(VERIFY_VECTORS, newline="", encoding="utf-8") as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + index, pks_str, msgs_str, sig_str, expected_result, comment = row + expected = (expected_result == "TRUE") + + try: + pks = [GE.from_bytes_xonly(b) for b in parse_hex_list(pks_str)] + msgs = parse_hex_list(msgs_str) + R, s = parse_sig(bytes.fromhex(sig_str)) + result = Verify(pks, msgs, (R, s)) + except ValueError: + result = False + + if result != expected: + failures.append(f"Verify #{index}: {comment}") + + return failures + + +def run_tweak_tests() -> List[str]: + failures = [] + + with open(TWEAK_VECTORS, newline="", encoding="utf-8") as f: + reader = csv.reader(f) + next(reader) + + for row in reader: + index, sk_str, t_str, is_xonly_str, exp_sk_str, exp_pk_str, comment = row + sk = Scalar.from_bytes_checked(bytes.fromhex(sk_str)) + t = Scalar.from_bytes_checked(bytes.fromhex(t_str)) + is_xonly = (is_xonly_str == "TRUE") + sk_out = TweakSK(sk, t, is_xonly) + pk_out = TweakPK(sk * G, t, is_xonly) + if (sk_out.to_bytes().hex().upper() != exp_sk_str + or pk_out.to_bytes_compressed().hex().upper() != exp_pk_str): + failures.append(f"Tweak #{index}: {comment}") + + return failures + + +def main(): + for filename in [SIGN_VECTORS, SIGN_ERROR_VECTORS, VERIFY_VECTORS, PARTIAL_SIG_VERIFY_VECTORS, TWEAK_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_sign_tests()) + failures.extend(run_sign_error_tests()) + failures.extend(run_partial_sig_verify_tests()) + failures.extend(run_verify_tests()) + failures.extend(run_tweak_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-0459/vectors/test_vectors_partial_sig_verify.csv b/bip-0459/vectors/test_vectors_partial_sig_verify.csv new file mode 100644 index 0000000000..fb1810bc80 --- /dev/null +++ b/bip-0459/vectors/test_vectors_partial_sig_verify.csv @@ -0,0 +1,10 @@ +index,signer_index,psig,pubkeys,messages,pubnonces,expected_result,comment +0,0,22B4D18FB61AAADDC4530460BAF0C50DD3F6185A2A8C1731846818E8B191057E,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,TRUE,Honest partial sig (signer 0) +1,1,6DBD27BD37C25736EA15AC8D877B004AC955B15D284D61E1FC81C72D56E7EA77,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,TRUE,Honest partial sig (signer 1) +2,1,22B4D18FB61AAADDC4530460BAF0C50DD3F6185A2A8C1731846818E8B191057E,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,Honest psig verified at the wrong signer_index +3,0,22B4D18FB61AAADDC4530460BAF0C50DD3F6185A2A8C1731846818E8B191057F,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,Tampered partial sig (bit flip) +4,0,0000000000000000000000000000000000000000000000000000000000000000,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,"psig = 0 (in range, wrong)" +5,0,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,psig = n (out of range) +6,2,22B4D18FB61AAADDC4530460BAF0C50DD3F6185A2A8C1731846818E8B191057E,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,signer_index out of range +7,0,80BAC2C1FAA8F295F513598F0E7B01EAFD05A96B20AC7D9B8A2F2485BD220BEA,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,psig for a different aggregate nonce +8,0,22B4D18FB61AAADDC4530460BAF0C50DD3F6185A2A8C1731846818E8B191057E,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;034D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076603F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A,FALSE,Aggregate nonce R1 is infinity diff --git a/bip-0459/vectors/test_vectors_sign.csv b/bip-0459/vectors/test_vectors_sign.csv new file mode 100644 index 0000000000..587399b9d3 --- /dev/null +++ b/bip-0459/vectors/test_vectors_sign.csv @@ -0,0 +1,8 @@ +index,secret_keys,messages,secnonces,expected_aggnonce,expected_partial_sigs,expected_sig,comment +0,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,8080808080808080808080808080808080808080808080808080808080808080,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337,6C107E8B714555051F1D56637B928675AE215314BBE5115629A6E30984313C95,5574D2BA5AB891F9E5BA99F360FE4598AE8B718CC7E4C60773FB7DE1C615C0C96C107E8B714555051F1D56637B928675AE215314BBE5115629A6E30984313C95,Single signer +1,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040;FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFAB6AAD8E2AB449C37BBCE5A88CC323D3D,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303;05050505050505050505050505050505050505050505050505050505050505050606060606060606060606060606060606060606060606060606060606060606,02989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F0256B328B30C8BF5839E24058747879408BDB36241DC9C2E7C619FAA12B2920967,22B4D18FB61AAADDC4530460BAF0C50DD3F6185A2A8C1731846818E8B191057E;6DBD27BD37C25736EA15AC8D877B004AC955B15D284D61E1FC81C72D56E7EA77,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,Two signers +2,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040;FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFAB6AAD8E2AB449C37BBCE5A88CC323D3D;0707070707070707070707070707070707070707070707070707070707070707,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303;05050505050505050505050505050505050505050505050505050505050505050606060606060606060606060606060606060606060606060606060606060606;08080808080808080808080808080808080808080808080808080808080808080909090909090909090909090909090909090909090909090909090909090909,021A7A569E91DBF60581509C7FC946D1003B60C7DEE85299538DB6353538D59574036360E856310CE5D294E8BE33FC807077DC56AC80D95D9CD4DDBD21325EFF73F7,421AD011362C0B2E71D6568EBB9BCDF35E30919057D3215C808DA42DAB81A2F2;17A303153A3D20CBDB7012C9E3D1687AF24B350D9419B15143EDE5BEC3095410;654E63BB8B84019B39B72DAA5D9B4C15D1F1A8507CB8B71E6BA84C987F996178,AFFDF803D48FC2160A7D14F23F3F9574FE3AF994E932DC0D0B20DBF208BDD0ECBF0C36E1FBED2D9586FD9702FD088284226D6EEE68A589CC3023D684EE24587A,Three signers +3,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040;FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFAB6AAD8E2AB449C37BBCE5A88CC323D3D;0707070707070707070707070707070707070707070707070707070707070707;F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F4B0A4D2DCA53E9631B5C85482C62C3737;0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282;8383838383838383838383838383838383838383838383838383838383838383;8484848484848484848484848484848484848484848484848484848484848484,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303;05050505050505050505050505050505050505050505050505050505050505050606060606060606060606060606060606060606060606060606060606060606;08080808080808080808080808080808080808080808080808080808080808080909090909090909090909090909090909090909090909090909090909090909;0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C;0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F,02BA72A6E8BA53E8B971AD0C9823968AEF4D78CE8AF255AB43DFF83003C902FB8D03948B53DA97FDF674C0877315ACBCC8761AA3B9A582B439982FBAFAC99F97210F,64DDCF219021686517B6422A9907FE4DEAF69FE85297BA598B52D676D9E2F388;716ACB885CE48B446E2747EC0D2749DA8F7F083B5A6C623EE258D5319A1562B8;FBB2F763A33AC9737D6533BDDDF1E749A3E2901F13EFFD00AA46A09EDA52C15C;7B98D36457540E385005224413EABC9D8465A483657916BEE37C17F6EDB5E2A7;A56EE5404D3E8C45BCC5EB07274FD20EDB62F5DE09185206E9958C760012112D,725ED0EA198C1B6144774932FF1DBC77B190299023BC6A3F3C662186F12DBC7FF3034AB234D3579B100DCB1FBF5BBE2108C318D6D0F441E7655F339A9BA688EE,Five signers +4,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040;FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0;A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1,0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C;0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E0E,0329CE758BB2D21DEF214CEA069ADDBD16896EC77924BF0EC9219E00DCF077605E02DCEC806DEB8DA0774EC5A5D0F5097FFDE1F70A9F40D44363BD3DC2DC2590CA5D,856B2743B7833E2EB0B67109CCD1154326B17DAF86BFB4E7EBF20297EA1FA642;9EF60AB27AA05AE6C12E1506B152A41C7B84B1319B946451A53D1602B32AA030,4937FA0FBED62408A9015A2447B4C61024964D557D0940988F0460E9BEEA9D82246131F63223991571E486107E23B960E78751FA730B78FDD15CBA0DCD140531,Same pubkey signs two different messages +5,2020202020202020202020202020202020202020202020202020202020202020;1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E1E,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303;05050505050505050505050505050505050505050505050505050505050505050606060606060606060606060606060606060606060606060606060606060606,02989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F0256B328B30C8BF5839E24058747879408BDB36241DC9C2E7C619FAA12B2920967,BDCFA7766B417E05EB217643F00F60AA4CF0FA5AE4287FB0CAD439D8FA351DA6;28301B09000D4CF033BD7AA51F2DF6FD0A72BA138ED2239A175083444DAE5BC6,7086C4FEE3014A26E29538913B791B72D4DCE742561B92D621E6B2E4B3DEE2A4E5FFC27F6B4ECAF61EDEF0E90F3D57A75763B46E72FAA34AE224BD1D47E3796C,Signers with tweaked keys +6,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040;FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0;C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0,14141414141414141414141414141414141414141414141414141414141414141515151515151515151515151515151515151515151515151515151515151515;16161616161616161616161616161616161616161616161616161616161616161717171717171717171717171717171717171717171717171717171717171717,035BE5E9478209674A96E60F1F037F6176540FD001FA1D64694770C56A7709C42C028F5304E2373E56EE7D774CB89E9F1AFECF0EE7E3E3757F189908F069DAA36C60,69BAC5757B4C51E7B3008CBD28F3FE38E03D90EFDC002B4C41037A0CA381B6EB;0D32BD47C6AD271927903F6448B02BBE533BBFB1C8273E7FB0235886239B04A9,45D4DB5AE5DE74A9F04CF404D434031A429C694BBAE21961F2B79AE0353093BF76ED82BD41F97900DA90CC2171A429F7337950A1A42769CBF126D292C71CBB94,"Exact duplicate (pk, m) entries" diff --git a/bip-0459/vectors/test_vectors_sign_error.csv b/bip-0459/vectors/test_vectors_sign_error.csv new file mode 100644 index 0000000000..be6dc08480 --- /dev/null +++ b/bip-0459/vectors/test_vectors_sign_error.csv @@ -0,0 +1,5 @@ +index,secret_key,message,secnonce,pubkeys,messages,pubnonces,comment +0,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,8080808080808080808080808080808080808080808080808080808080808080,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A;03F991F944D1E1954A7FC8B9BF62E0D78F015F4C07762D505E20E6C45260A3661B02531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337,Signer's R2 appears at two indices +1,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,8080808080808080808080808080808080808080808080808080808080808080,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766031195A8046DCBB8E17034BCA630065E7A0982E4E36F6F7E5A8D4554E4846FCD99;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A;03F991F944D1E1954A7FC8B9BF62E0D78F015F4C07762D505E20E6C45260A3661B0256B328B30C8BF5839E24058747879408BDB36241DC9C2E7C619FAA12B2920967,Signer's R2 appears at no index +2,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,8080808080808080808080808080808080808080808080808080808080808080,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F,EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A;03F991F944D1E1954A7FC8B9BF62E0D78F015F4C07762D505E20E6C45260A3661B0256B328B30C8BF5839E24058747879408BDB36241DC9C2E7C619FAA12B2920967,Message at signer's index was substituted +3,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,8080808080808080808080808080808080808080808080808080808080808080,02020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303,9AC20335EB38768D2052BE1DBBC3C8F6178407458E51E6B4AD22F1D91758895B;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282,024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D076602531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337;0362C0A046DACCE86DDD0343C6D3C7C79C2208BA0D9C9CF24A6D046D21D21F90F703F006A18D5653C4EDF5391FF23A61F03FF83D237E880EE61187FA9F379A028E0A;03F991F944D1E1954A7FC8B9BF62E0D78F015F4C07762D505E20E6C45260A3661B0256B328B30C8BF5839E24058747879408BDB36241DC9C2E7C619FAA12B2920967,Public key at signer's index was substituted diff --git a/bip-0459/vectors/test_vectors_tweak.csv b/bip-0459/vectors/test_vectors_tweak.csv new file mode 100644 index 0000000000..fcaa22b6b4 --- /dev/null +++ b/bip-0459/vectors/test_vectors_tweak.csv @@ -0,0 +1,5 @@ +index,internal_secret_key,tweak,is_xonly,expected_secret_key,expected_pubkey,comment +0,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,0202020202020202020202020202020202020202020202020202020202020202,FALSE,0101010101010101010101010101010101010101010101010101010101010101,031B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,Plain tweak of an even-y key +1,0101010101010101010101010101010101010101010101010101010101010101,0202020202020202020202020202020202020202020202020202020202020202,FALSE,0303030303030303030303030303030303030303030303030303030303030303,02531FE6068134503D2723133227C867AC8FA6C83C537E9A44C3C5BDBDCB1FE337,Plain tweak of an odd-y key +2,FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFDB9ADDBE5AE479F3ABED15D8BCF354040,0202020202020202020202020202020202020202020202020202020202020202,TRUE,0101010101010101010101010101010101010101010101010101010101010101,031B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,X-only tweak of an even-y key +3,0101010101010101010101010101010101010101010101010101010101010101,0202020202020202020202020202020202020202020202020202020202020202,TRUE,0101010101010101010101010101010101010101010101010101010101010101,031B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,X-only tweak of an odd-y key diff --git a/bip-0459/vectors/test_vectors_verify.csv b/bip-0459/vectors/test_vectors_verify.csv new file mode 100644 index 0000000000..5debb48507 --- /dev/null +++ b/bip-0459/vectors/test_vectors_verify.csv @@ -0,0 +1,16 @@ +index,pubkeys,messages,sig,expected_result,comment +0,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,TRUE,Two-signer valid aggregate +1,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;989C0B76CB563971FDC9BEF31EC06C3560F3249D6EE9E5D83C57625596E05F6F,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181;8282828282828282828282828282828282828282828282828282828282828282,AFFDF803D48FC2160A7D14F23F3F9574FE3AF994E932DC0D0B20DBF208BDD0ECBF0C36E1FBED2D9586FD9702FD088284226D6EEE68A589CC3023D684EE24587A,TRUE,Three-signer valid aggregate +2,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,Wrong message for second signer +3,462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B;1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,Pubkey order swapped +4,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F,8080808080808080808080808080808080808080808080808080808080808080,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,Dropped second signer +5,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF4,FALSE,Tampered s (bit flip) +6,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC0000000000000000000000000000000000000000000000000000000000000000,FALSE,"s = 0 (in range, wrong)" +7,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,s = n (out of range) +8,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FALSE,s = 2^256 - 1 +9,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,03030303030303030303030303030303030303030303030303030303030303039071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,R x-coord not on curve +10,0303030303030303030303030303030303030303030303030303030303030303;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,Public key not on curve +11,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EF,FALSE,Signature too short +12,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080;8181818181818181818181818181818181818181818181818181818181818181,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF500,FALSE,Signature too long +13,1B84C5567B126440995D3ED5AABA0565D71E1834604819FF9C17F5E9D5DD078F;462779AD4AAD39514614751A71085F2F10E1C7A593E4E030EFB5B8721CE55B0B,8080808080808080808080808080808080808080808080808080808080808080,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,Pubkey/message count mismatch +14,,,8FB60AB39F708AFEE79AD25E6F5528BF4FCC5E56083E82417357826E19E250BC9071F94CEDDD0214AE68B0EE426BC5589D4BC9B752D9791380E9E0160878EFF5,FALSE,Empty signer list From 557cb05093701961deb34abc2bd500f82f7e1de4 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 11 Jul 2026 18:03:00 +0200 Subject: [PATCH 3/3] BIP459: Add entry to the README index --- README.mediawiki | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.mediawiki b/README.mediawiki index 140eed3076..4ac4e76ccd 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-0459.mediawiki|459]] +| Applications +| Full-Aggregation of BIP 340 Signatures +| Fabian Jahr +| Specification +| Draft |}