Source code for btclib_secp256k1.keys

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""Secp256k1 key and point algebra.

These are the libsecp256k1 secret and public key operations, i.e. the
scalar and point arithmetic underlying key derivation (BIP32) and key
aggregation; libsecp256k1 calls a private key a secret key, hence its
seckey function names.

Public keys are returned in compressed form, unless otherwise required.
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import overload

from . import BytesLike, CData, MutableBytesLike, ffi, lib
from ._cdata import array
from ._scalar import octets, scalar, sequence
from ._secret import scalar_buffer, take, wipe
from .context import _checked, ctx

__all__ = [
    "COMPRESSED",
    "UNCOMPRESSED",
    "PubkeyTweakChain",
    "parse",
    "prvkey_negate",
    "prvkey_tweak_add",
    "prvkey_tweak_mul",
    "prvkey_verify",
    "pubkey_cmp",
    "pubkey_combine",
    "pubkey_from_prvkey",
    "pubkey_negate",
    "pubkey_sort",
    "pubkey_sum",
    "pubkey_tweak_add",
    "pubkey_tweak_mul",
    "pubkey_tweak_mul_sum",
    "pubkey_verify",
    "reserialize",
    "serialize",
]

# SECP256K1_EC_COMPRESSED and SECP256K1_EC_UNCOMPRESSED: the
# libsecp256k1 flag macros do not survive the preprocessing of the
# headers into cffi definitions
COMPRESSED = 258
UNCOMPRESSED = 2

# the buffers `serialize` writes into, and the lengths it declares them
# with. The pairing is what these are for: each type is `ffi.typeof` of the
# width beside it, so the buffer and the length cannot say different
# numbers, and neither is `ffi.sizeof` of a cdata per call. This is the one
# spelling every unpacked buffer in the package uses; `xonly.py` carries
# the session and the reason.
#
# It is not hoisted for the `ffi.new`. cffi already caches the parse of
# a literal cdecl, so a hoisted type beats one by 0.003 microseconds --
# real, 4.3% on a noise row of 0.5%, and an order below the 0.054 an
# interpolated cdecl gives back. What asks for a name here is the length,
# and the type comes along with it: so the length pointer below stays
# spelled in full, like every cdecl whose buffer nothing unpacks
_COMPRESSED_SIZE = 33
_COMPRESSED_BUFFER_TYPE = ffi.typeof(f"char[{_COMPRESSED_SIZE}]")
_UNCOMPRESSED_SIZE = 65
_UNCOMPRESSED_BUFFER_TYPE = ffi.typeof(f"char[{_UNCOMPRESSED_SIZE}]")


[docs] def prvkey_verify(prvkey: BytesLike | int) -> bool: """Return True if the private key is a valid scalar, i.e. in [1, n-1]. Args: prvkey: the private key, 32 bytes or an int below 2**256. Returns: True if it is in [1, n-1]; False for zero, for anything at or above the group order, for octets of any other length and for an int that does not fit in 32 bytes. Those are verdicts and not errors: the length is part of the verdict, as in `pubkey_verify`. Raises: TypeError: if the value is neither bytes nor an int, which is a malformed argument and not a key to have a verdict on. """ try: prvkey_bytes = scalar(prvkey, "private key") except ValueError: return False return bool(lib.secp256k1_ec_seckey_verify(ctx, prvkey_bytes))
[docs] def pubkey_verify(pubkey_bytes: BytesLike) -> bool: """Return True if the octets are a public key libsecp256k1 accepts. The public-key twin of `prvkey_verify`, and the call for a library validating a key at its own boundary: `secp256k1_ec_pubkey_parse` is the proof, and this is that proof with nothing kept. `parse` hands back an object whose lifetime becomes the caller's, and `reserialize` hands back octets it already had -- 0.37 us of serialization for an answer that was in the argument. A verdict and not an exception, as `prvkey_verify` is: what the octets are wrong about is the caller's to phrase, and a library validating an input has its own word for it. Args: pubkey_bytes: the public key, 33 or 65 bytes. Returns: True if it is a point of the curve in either serialization; False for octets of any other length too, which is a verdict here where every other entry point taking a key raises: there is nothing to do with such a key either way, and a caller asking whether it has one has asked about the length as well. Raises: TypeError: if the value is not bytes at all, which is a malformed argument and not a key to have a verdict on. """ return _parsed(pubkey_bytes, "public key") is not None
@overload def prvkey_negate(prvkey: BytesLike | int) -> bytes: ... @overload def prvkey_negate(prvkey: BytesLike | int, *, into: MutableBytesLike) -> None: ... @overload def prvkey_negate( prvkey: BytesLike | int, *, into: MutableBytesLike | None ) -> bytes | None: ...
[docs] def prvkey_negate( prvkey: BytesLike | int, *, into: MutableBytesLike | None = None ) -> bytes | None: """Negate a private key. Args: prvkey: the private key, 32 bytes or an int below 2**256. into: a writable 32-byte buffer to receive the result, instead of the `bytes` this otherwise returns. See `_secret.take` and SECURITY.md for what that does and does not buy. Returns: The 32 bytes of n - k, the key of the negated public key -- or None where `into` was given and holds them. Raises: TypeError: if `into` is not a writable bytearray or memoryview of octets. ValueError: if it is not 32 bytes, does not fit in them, or is not in [1, n-1]; or if `into` is not 32 bytes. """ prvkey_buffer = scalar_buffer(prvkey, "private key") if not lib.secp256k1_ec_seckey_negate(ctx, prvkey_buffer): raise ValueError("invalid private key: not in [1, n-1]") return take(prvkey_buffer, into=into)
@overload def prvkey_tweak_add(prvkey: BytesLike | int, tweak: BytesLike | int) -> bytes: ... @overload def prvkey_tweak_add( prvkey: BytesLike | int, tweak: BytesLike | int, *, into: MutableBytesLike ) -> None: ... @overload def prvkey_tweak_add( prvkey: BytesLike | int, tweak: BytesLike | int, *, into: MutableBytesLike | None ) -> bytes | None: ...
[docs] def prvkey_tweak_add( prvkey: BytesLike | int, tweak: BytesLike | int, *, into: MutableBytesLike | None = None, ) -> bytes | None: """Add a tweak to a private key. This is the private-key side of BIP32 derivation, and of any other scheme adding a scalar to a key. `xonly.prvkey_tweak_add` is the BIP341 one, which negates the key first where its point has odd y. Args: prvkey: the private key, 32 bytes or an int below 2**256. tweak: the tweak, 32 bytes or an int below 2**256. into: a writable 32-byte buffer to receive the result, instead of the `bytes` this otherwise returns. See `_secret.take` and SECURITY.md for what that does and does not buy. Returns: The 32 bytes of (k + t) mod n -- or None where `into` was given and holds them. Raises: TypeError: if `into` is not a writable bytearray or memoryview of octets. ValueError: if either value is not 32 bytes or does not fit in them, if the private key is not in [1, n-1], if the sum is zero, which is the one tweak with no valid result, or if `into` is not 32 bytes. """ prvkey_buffer = scalar_buffer(prvkey, "private key") try: tweak_bytes = scalar(tweak, "tweak") if not lib.secp256k1_ec_seckey_tweak_add(ctx, prvkey_buffer, tweak_bytes): raise ValueError("invalid private key or tweak") return take(prvkey_buffer, into=into) finally: # `scalar` validates the tweak after the copy above is made, so a # tweak it refuses never reaches libsecp256k1 -- whose own # zeroing on a refusal is what the call above would otherwise # rely on -- and the copy is this package's to wipe instead. # `take` already does so on every path it is reached by; this is # only for the two it is not wipe(prvkey_buffer)
@overload def prvkey_tweak_mul(prvkey: BytesLike | int, tweak: BytesLike | int) -> bytes: ... @overload def prvkey_tweak_mul( prvkey: BytesLike | int, tweak: BytesLike | int, *, into: MutableBytesLike ) -> None: ... @overload def prvkey_tweak_mul( prvkey: BytesLike | int, tweak: BytesLike | int, *, into: MutableBytesLike | None ) -> bytes | None: ...
[docs] def prvkey_tweak_mul( prvkey: BytesLike | int, tweak: BytesLike | int, *, into: MutableBytesLike | None = None, ) -> bytes | None: """Multiply a private key by a tweak. Args: prvkey: the private key, 32 bytes or an int below 2**256. tweak: the tweak, 32 bytes or an int below 2**256. into: a writable 32-byte buffer to receive the result, instead of the `bytes` this otherwise returns. See `_secret.take` and SECURITY.md for what that does and does not buy. Returns: The 32 bytes of (k * t) mod n -- or None where `into` was given and holds them. Raises: TypeError: if `into` is not a writable bytearray or memoryview of octets. ValueError: if either value is not 32 bytes or does not fit in them, if the private key is not in [1, n-1], if the tweak is zero or at or above the group order, or if `into` is not 32 bytes. """ prvkey_buffer = scalar_buffer(prvkey, "private key") try: tweak_bytes = scalar(tweak, "tweak") if not lib.secp256k1_ec_seckey_tweak_mul(ctx, prvkey_buffer, tweak_bytes): raise ValueError("invalid private key or tweak") return take(prvkey_buffer, into=into) finally: # see prvkey_tweak_add's identical finally for why this is owed # beside a call `take` already covers wipe(prvkey_buffer)
def _pubkey_from_prvkey_(prvkey: BytesLike | int) -> CData: """Return the public key of a private key, as the parsed point. The private half of `pubkey_from_prvkey`, for a caller who is about to hand the point to another wrapper rather than to hold its bytes: `xonly.from_prvkey` is the one this package makes. See the package docstring for what the two underscores mean throughout. Args: prvkey: the private key, 32 bytes or an int below 2**256. Returns: The libsecp256k1 public key object of the point kG. Raises: ValueError: if the private key is not 32 bytes, does not fit in them, or is not in [1, n-1]. """ pubkey = ffi.new("secp256k1_pubkey *") if not lib.secp256k1_ec_pubkey_create(ctx, pubkey, scalar(prvkey, "private key")): raise ValueError("invalid private key: not in [1, n-1]") return pubkey
[docs] def pubkey_from_prvkey(prvkey: BytesLike | int, compressed: bool = True) -> bytes: """Return the public key of a private key, i.e. the point kG. This is the generator multiplication, with the serialization flag this module's other producers all take: `compressed=False` answers the 65 octets of `0x04 || x || y`, which is the form a caller reading coordinates wants and the cheap one to parse back. Every private-to-public conversion that wants the compressed form -- BIP32 neutering, a fingerprint, an address -- is this call and nothing after it. Args: prvkey: the private key, 32 bytes or an int below 2**256. compressed: whether to return 33 bytes rather than 65. Returns: The serialized point kG -- 33 bytes whose first octet carries the parity of y, or the 65 bytes of 0x04 || x || y. Raises: ValueError: if the private key is not 32 bytes, does not fit in them, or is not in [1, n-1]. RuntimeError: if libsecp256k1 fails to serialize the point, which no valid key can make it do. Example: >>> from btclib_secp256k1 import keys >>> keys.pubkey_from_prvkey(1).hex()[:10] '0279be667e' """ return serialize(_pubkey_from_prvkey_(prvkey), compressed)
def _pubkey_negate_(pubkey: CData) -> CData: """Negate an already-parsed public key. The private half of `pubkey_negate`, for a caller who already holds the parsed point: see the package docstring for what the two underscores mean throughout. Args: pubkey: the already-parsed public key, as `parse` returns. Mutated in place. Returns: The same object passed in, negated. Raises: RuntimeError: if libsecp256k1 refuses the object -- a NULL pointer, or a `secp256k1_pubkey` nothing has written to -- or fails for any other reason, which a key it produced cannot make it do. Which of the two it was is on the thread rather than in the message, and `context.check` is what reads it. """ negated = lib.secp256k1_ec_pubkey_negate(ctx, pubkey) if not negated: msg = "public key negation failed" raise RuntimeError(msg) return pubkey
[docs] def pubkey_negate(pubkey_bytes: BytesLike, compressed: bool = True) -> bytes: """Negate a public key. Args: pubkey_bytes: the public key, 33 or 65 bytes. compressed: whether to return 33 bytes rather than 65. Returns: The point with the same x and the other y, serialized. Raises: ValueError: if the public key is not a valid point. RuntimeError: if libsecp256k1 fails to negate or serialize it, which no valid key can make it do. """ return serialize(_pubkey_negate_(parse(pubkey_bytes)), compressed)
def _pubkey_tweak_add_(pubkey: CData, tweak: BytesLike | int) -> CData: """Add the generator multiplied by the tweak, to an already-parsed key. The private half of `pubkey_tweak_add`, for a caller who already holds the parsed point and so has no parse left to redo: `PubkeyTweakChain` is the one, walking a BIP32 path one tweak at a time without parsing the point back out of its own serialization at every step. See the package docstring for what the two underscores mean throughout. Args: pubkey: the already-parsed public key, as `parse` returns. Mutated in place. tweak: the tweak, 32 bytes or an int below 2**256. Returns: The same object passed in, tweaked. Raises: ValueError: if the tweak is not 32 bytes or does not fit in them, if the tweak or the resulting public key is invalid, or if the object is not a public key libsecp256k1 will read, those last two being one message here -- `context.check` is what tells them apart. """ tweak_bytes = scalar(tweak, "tweak") tweaked = lib.secp256k1_ec_pubkey_tweak_add(ctx, pubkey, tweak_bytes) if not tweaked: raise ValueError("invalid tweak or resulting public key") return pubkey
[docs] def pubkey_tweak_add( pubkey_bytes: BytesLike, tweak: BytesLike | int, compressed: bool = True ) -> bytes: """Add the generator multiplied by the tweak to a public key. This is the public-key side of BIP32 derivation: the key of `prvkey_tweak_add(k, t)` is `pubkey_tweak_add(pubkey(k), t)`. Adding more than one tweak to the same key is `PubkeyTweakChain`, which parses the key once rather than once per tweak. Args: pubkey_bytes: the public key, 33 or 65 bytes. tweak: the tweak, 32 bytes or an int below 2**256. compressed: whether to return 33 bytes rather than 65. Returns: The serialized point P + tG. Raises: ValueError: if the public key is not a valid point, if the tweak is not 32 bytes or does not fit in them, or if the tweak or the resulting key is invalid. RuntimeError: if libsecp256k1 fails to serialize the result, which no valid input can make it do. """ # spelled out rather than composed of `parse`, `_pubkey_tweak_add_` # and `serialize`: those three frames are 0.037 microseconds of the # 3.403 this call costs -- an Apple M5, macOS 26.6, arm64, CPython # 3.13.14, the two spellings alternated in one process over 7 rounds # of 20 000 calls, minimum kept for each. The private half makes the # same calls for a caller holding the point, and # tests/parsed_keys_test.py asserts the two answer alike pubkey_bytes = octets(pubkey_bytes, "public key") pubkey = ffi.new("secp256k1_pubkey *") if not lib.secp256k1_ec_pubkey_parse(ctx, pubkey, pubkey_bytes, len(pubkey_bytes)): raise ValueError("invalid public key") if not lib.secp256k1_ec_pubkey_tweak_add(ctx, pubkey, scalar(tweak, "tweak")): raise ValueError("invalid tweak or resulting public key") return serialize(pubkey, compressed)
[docs] class PubkeyTweakChain: """Add a sequence of tweaks to a public key, parsing it only once. `pubkey_tweak_add` parses its argument and serializes its result, so a caller adding tweak after tweak to its own output -- a BIP32 path walked one index at a time, each needing the previous step's serialized key to hash into the next tweak -- re-parses at every step the very point the step before had already built, and only serialized because *that* step's caller needed the bytes. This holds the parsed point across the calls instead: the first tweak is the only one that pays for a parse, and every step still returns the bytes its caller needs. `pubkey` is that point without a tweak added, for a caller that has reached the end of the path and wants the key it arrived at, or that wants the key it started from in the other serialization. Args: pubkey_bytes: the public key the chain starts from, 33 or 65 bytes. Raises: ValueError: if the public key is not a valid point. Example: >>> from btclib_secp256k1 import keys >>> generator = keys.pubkey_from_prvkey(1, compressed=False) >>> chain = keys.PubkeyTweakChain(generator) >>> step1 = chain.tweak_add(2) >>> step2 = chain.tweak_add(3) >>> step1 == keys.pubkey_tweak_add(generator, 2) True >>> step2 == keys.pubkey_tweak_add(step1, 3) True >>> chain.pubkey() == step2 True """ # pydoclint (DOC301) asks that this carry no docstring of its own, # the class docstring above being where the constructor is documented def __init__(self, pubkey_bytes: BytesLike) -> None: # noqa: D107 self._pubkey = parse(pubkey_bytes)
[docs] def tweak_add(self, tweak: BytesLike | int, compressed: bool = True) -> bytes: """Add the generator multiplied by the tweak, to the held key. Args: tweak: the tweak, 32 bytes or an int below 2**256. compressed: whether to return 33 bytes rather than 65. Returns: The serialized point, with this tweak and every earlier one already added. Raises: ValueError: if the tweak is not 32 bytes or does not fit in them, or if the tweak or the resulting key is invalid -- which ends the chain. libsecp256k1 says of `secp256k1_ec_pubkey_tweak_add` that "pubkey will be set to an invalid value if this function returns 0", and the point this holds is that pubkey: there is nothing left to step from, so a caller catching this builds a chain afresh rather than asking again. RuntimeError: if libsecp256k1 fails to serialize the result, which no valid input can make it do. """ return serialize(_pubkey_tweak_add_(self._pubkey, tweak), compressed)
[docs] def pubkey(self, compressed: bool = True) -> bytes: """Return the key the chain holds, with no tweak added. Returns: The serialized point, with every tweak added so far. Before the first `tweak_add` that is the key the chain was built from, which is how this doubles as the `reserialize` of it. Args: compressed: whether to return 33 bytes rather than 65. Raises: RuntimeError: if libsecp256k1 fails to serialize it, which no valid input can make it do. """ return serialize(self._pubkey, compressed)
def _pubkey_(self) -> CData: """Return the parsed point the chain holds. What `pubkey` answers in octets, for a caller handing the point to a private half rather than holding its bytes -- the end of a BIP32 path that is about to be tweaked into a taproot output key, `xonly._tweak_add_` being where that goes. Returns: The libsecp256k1 public key object, which is the chain's own and which the next `tweak_add` mutates. """ return self._pubkey
def _pubkey_tweak_mul_(pubkey: CData, tweak: BytesLike | int) -> CData: """Multiply an already-parsed public key by a tweak. The private half of `pubkey_tweak_mul`, for a caller who already holds the parsed point: see the package docstring for what the two underscores mean throughout. This is the shared point of an ECDH exchange: `ecdh._shared_point_` is the same point from the same parsed key, in constant time, and `ecdh._shared_secret_` the hash of it. Args: pubkey: the already-parsed public key, as `parse` returns. Mutated in place. tweak: the scalar to multiply by, 32 bytes or an int below 2**256. Returns: The same object passed in, multiplied. Raises: ValueError: if the tweak is not 32 bytes or does not fit in them, if it is zero or at or above the group order, or if the object is not a public key libsecp256k1 will read, those last two being one message here -- `context.check` is what tells them apart. """ tweak_bytes = scalar(tweak, "tweak") multiplied = lib.secp256k1_ec_pubkey_tweak_mul(ctx, pubkey, tweak_bytes) if not multiplied: raise ValueError("invalid tweak") return pubkey
[docs] def pubkey_tweak_mul( pubkey_bytes: BytesLike, tweak: BytesLike | int, compressed: bool = True ) -> bytes: """Multiply a public key by a tweak. This is the multiplication of an arbitrary point, as opposed to the multiplication of the generator `pubkey_from_prvkey` is, and is the shared point of an ECDH exchange: see `ecdh.shared_secret`, which hashes it. **It is not constant time**: `secp256k1_ec_pubkey_tweak_mul` runs `secp256k1_ecmult`, the wNAF multiplication, which is variable time in the tweak -- unlike `secp256k1_ecmult_const`, which is what `secp256k1_ecdh` calls to reach that same point. `ecdh.shared_point` is that call answering the point, and the one for a tweak that is a secret. Args: pubkey_bytes: the public key, 33 or 65 bytes. tweak: the scalar to multiply by, 32 bytes or an int below 2**256. compressed: whether to return 33 bytes rather than 65. Returns: The serialized point tP. Raises: ValueError: if the public key is not a valid point, if the tweak is not 32 bytes or does not fit in them, or if it is zero or at or above the group order. RuntimeError: if libsecp256k1 fails to serialize the result, which no valid input can make it do. """ return serialize(_pubkey_tweak_mul_(parse(pubkey_bytes), tweak), compressed)
def _pubkey_combine_(pubkeys: Sequence[CData]) -> CData: """Add already-parsed public keys together. The private half of `pubkey_combine`, and the one that answers with the sum rather than with its serialization: see the package docstring for what the two underscores mean throughout. `_pubkey_sort_` is what hands the keys over in the order BIP67 and MuSig2 ask for, and the two together are an aggregation that parses each key once and serializes once, where the public halves serialize every sorted key only to parse it back. Args: pubkeys: the already-parsed public keys, as `parse` returns. At least one is required. Returns: The libsecp256k1 public key object of the sum. Raises: ValueError: if the sequence is empty, if any object is not a public key libsecp256k1 will read, or if the sum is the point at infinity, which is no public key. """ combined = _pubkey_sum_(pubkeys) if combined is None: raise ValueError("invalid public key sum") return combined def _pubkey_sum_(pubkeys: Sequence[CData]) -> CData | None: """Add already-parsed public keys together, infinity included. The private half of `pubkey_sum`, and what `_pubkey_combine_` is built on: see the package docstring for what the two underscores mean throughout. Args: pubkeys: the already-parsed public keys, as `parse` returns. At least one is required. Returns: The libsecp256k1 public key object of the sum, or None where that sum is the point at infinity. Raises: ValueError: if the sequence is empty, or if any object is not a public key libsecp256k1 will read -- a NULL pointer, or a `secp256k1_pubkey` nothing has written to -- the message then being libsecp256k1's own, as `context.check` words it. """ pubkeys = list(pubkeys) if not pubkeys: raise ValueError("at least one public key is required") combined = ffi.new("secp256k1_pubkey *") # a key libsecp256k1 cannot read can still sum to a point and return # 1, so the refusal is read from the illegal callback, and a 0 left # after that is the point at infinity summed = _checked( lib.secp256k1_ec_pubkey_combine, ctx, combined, array("secp256k1_pubkey *[]", pubkeys), len(pubkeys), ) return combined if summed else None
[docs] def pubkey_combine( pubkeys_bytes: Sequence[BytesLike], compressed: bool = True ) -> bytes: """Add public keys together. Args: pubkeys_bytes: the public keys, each 33 or 65 bytes. At least one is required. compressed: whether to return 33 bytes rather than 65. Returns: The serialized sum of the points. Raises: ValueError: if the sequence is empty, if any key is not a valid point, or if the sum is the point at infinity, which has no serialization. RuntimeError: if libsecp256k1 fails to serialize the result, which no valid input can make it do. """ return serialize( _pubkey_combine_([parse(pubkey_bytes) for pubkey_bytes in pubkeys_bytes]), compressed, )
[docs] def pubkey_sum( pubkeys_bytes: Sequence[BytesLike], compressed: bool = True ) -> bytes | None: """Add public keys together, answering None for the point at infinity. `pubkey_combine` with the one sum that is no public key answered rather than refused. A caller doing arithmetic has that sum as a value -- `P + (-P)` is the identity and not a malformed argument -- and the two spellings are the same call. The infinity has no serialization, which is why it is None and not octets: a `secp256k1_pubkey` is a point of the curve and never the identity, so there is nothing for this to hand back. Args: pubkeys_bytes: the public keys, each 33 or 65 bytes. At least one is required. compressed: whether to return 33 bytes rather than 65. Returns: The serialized sum of the points, or None where they sum to the point at infinity. Raises: ValueError: if the sequence is empty, or if any key is not a valid point. RuntimeError: if libsecp256k1 fails to serialize the result, which no valid input can make it do. Example: >>> from btclib_secp256k1 import keys >>> pubkey = keys.pubkey_from_prvkey(7) >>> keys.pubkey_sum([pubkey, keys.pubkey_negate(pubkey)]) is None True >>> keys.pubkey_sum([pubkey]) == pubkey True """ summed = _pubkey_sum_([parse(pubkey_bytes) for pubkey_bytes in pubkeys_bytes]) return None if summed is None else serialize(summed, compressed)
[docs] def pubkey_tweak_mul_sum( pubkeys_bytes: Sequence[BytesLike], tweaks: Sequence[BytesLike | int], compressed: bool = True, ) -> bytes | None: """Multiply each public key by its tweak and add the products. The names it is spelled with, in that order: a `pubkey_tweak_mul` per pair and one `pubkey_sum` over the products. That is the multi-scalar multiplication a caller writes as a verification equation -- u*H + v*Q of ECDSA and BIP340, MuSig2's aggregate of a key per signer, BIP352's tweak data -- and the whole of what it adds is that no product is serialized: written with the public halves, each `pubkey_tweak_mul` serializes 65 octets that the sum then parses again, and the terms are the only place a caller has to put them. The naive form of it, deliberately: a term at a time and one sum, with none of the shared precomputation of Strauss or Pippenger. libsecp256k1 has `secp256k1_ecmult_multi_var`, which is internal and not declared in `include/secp256k1.h`, so this is the only multi-scalar multiplication the public API can be composed of -- and the saving here is the crossing rather than the arithmetic, flat in the number of terms where a batched algorithm would grow with it. **This is the second function here that is more than one libsecp256k1 decision**, `xonly.from_prvkey` being the first, and the README's Design section says why the rule is what it is: the boundary computes nothing of its own. Neither does this -- the terms are `secp256k1_ec_pubkey_tweak_mul` and the sum is `secp256k1_ec_pubkey_combine`, in the one order that spells the equation -- and what is saved is a serialization and a parse per term, about a seventh of the call from three terms up and a little less at two. `PubkeyTweakChain` is an exception of the other kind and not of this one: its `tweak_add` is one decision and a serialization like every other, and what it holds is a parsed key across calls the caller makes one at a time. The infinity is `pubkey_sum`'s None, for the reason it is there and is more likely here: a verification equation is written to land on it, u*H + v*Q with v*Q the negation of u*H being the accepting case of a check spelled as a difference. Args: pubkeys_bytes: the public keys, each 33 or 65 bytes. At least one is required, as the sum requires it. tweaks: the scalar each key is multiplied by, one per key, 32 bytes or an int below 2**256. compressed: whether to return 33 bytes rather than 65. Returns: The serialized sum of the products, or None where they sum to the point at infinity. Raises: TypeError: if `tweaks` is a `str`, a cffi array of primitives or a one-dimensional buffer of anything but byte-strings, objects or structs (`bytes`, `bytearray`, `memoryview`, `array.array`): one tweak, not a list of them. ValueError: if the two sequences are of different lengths, if the sequence of keys is empty, if any key is not a valid point, or if any tweak is not 32 bytes, does not fit in them, or multiplies its key to something invalid -- zero and the group order being the two scalars there is no product for. RuntimeError: if libsecp256k1 fails to serialize the result, which no valid input can make it do. Example: >>> from btclib_secp256k1 import keys >>> generator = keys.pubkey_from_prvkey(1) >>> pubkey = keys.pubkey_from_prvkey(7) >>> # 2*G + 3*(7*G) is 23*G >>> total = keys.pubkey_tweak_mul_sum([generator, pubkey], [2, 3]) >>> total == keys.pubkey_from_prvkey(23) True >>> # and a sum landing on the identity is None, not a refusal >>> negated = keys.pubkey_negate(pubkey) >>> keys.pubkey_tweak_mul_sum([pubkey, negated], [3, 3]) is None True """ sequence(tweaks, "tweaks") if len(pubkeys_bytes) != len(tweaks): msg = ( f"as many tweaks as public keys are required: " f"{len(tweaks)} tweaks, {len(pubkeys_bytes)} public keys" ) raise ValueError(msg) # `strict=False` and the check above rather than `strict=True` and # no check: a sequence has a length, so the two are the same guard, # and the one that raises here says which sequence was short and by # how much where zip says only that they differed. A guard no input # can reach is one no test can drive, which this package does not # leave behind products = [ _pubkey_tweak_mul_(parse(pubkey_bytes), tweak) for pubkey_bytes, tweak in zip(pubkeys_bytes, tweaks, strict=False) ] summed = _pubkey_sum_(products) return None if summed is None else serialize(summed, compressed)
def _pubkey_cmp_(pubkey1: CData, pubkey2: CData) -> int: """Compare two already-parsed public keys, in compressed-form order. The private half of `pubkey_cmp`, for a caller who already holds both parsed points -- sorting keys it has parsed for another reason, where every comparison of a sort would otherwise parse both of its arguments again. See the package docstring for what the two underscores mean throughout. Args: pubkey1: the first already-parsed public key, as `parse` returns. pubkey2: the second one. Returns: A negative number, zero, or a positive number, according to whether the first key sorts before, equal to, or after the second. Where libsecp256k1 cannot read an object it compares a key of zeros in its place, so the answer is an ordering like any other and means nothing: this raises nothing, and `context.check` immediately after the call is what says the answer is not to be believed. `pubkey_cmp` parses what it is given and so has no such case. """ order = lib.secp256k1_ec_pubkey_cmp(ctx, pubkey1, pubkey2) return int(order)
[docs] def pubkey_cmp(pubkey1_bytes: BytesLike, pubkey2_bytes: BytesLike) -> int: """Compare two public keys, in lexicographic order of compressed form. The order is the one of the compressed serialization, whichever form the arguments are given in. Args: pubkey1_bytes: the first public key, 33 or 65 bytes. pubkey2_bytes: the second public key, 33 or 65 bytes. Returns: A negative number, zero, or a positive number, according to whether the first key sorts before, equal to, or after the second. Raises: ValueError: if either key is not a valid point. """ return _pubkey_cmp_(parse(pubkey1_bytes), parse(pubkey2_bytes))
def _pubkey_sort_(pubkeys: Sequence[CData]) -> list[CData]: """Sort already-parsed public keys, in compressed-form order. The private half of `pubkey_sort`, and the one that answers with the keys rather than with their serializations: see the package docstring for what the two underscores mean throughout. Sorting in order to aggregate is this and `_pubkey_combine_`, which takes what this returns. Args: pubkeys: the already-parsed public keys, as `parse` returns. An empty sequence sorts to an empty list. Returns: The same objects that were passed in, in ascending order. Raises: ValueError: if any object is not a public key libsecp256k1 will read -- a NULL pointer, or a `secp256k1_pubkey` nothing has written to -- the message then being libsecp256k1's own, as `context.check` words it. """ pubkeys = list(pubkeys) # nothing to sort is not a call to make: the array of an empty # sequence is the NULL `_cdata.array` answers, which is what # libsecp256k1 wants beside a count of zero, and there is no # ordering to read back out of it if not pubkeys: return [] # the array holds borrowed pointers, and is what gets reordered: the # list above is what keeps the keys it points to alive pointers = array("secp256k1_pubkey *[]", pubkeys) # it compares a key it cannot read as 33 zero octets and returns 1, # so the refusal is read from the illegal callback. It returns 0 only # for a NULL array or element, which that callback reports first _checked(lib.secp256k1_ec_pubkey_sort, ctx, pointers, len(pubkeys)) # the sort compares pairs, so a lone key is never read by it; the # comparison of the key with itself is what reads it if len(pubkeys) == 1: _checked(lib.secp256k1_ec_pubkey_cmp, ctx, pubkeys[0], pubkeys[0]) # what comes back are the caller's own objects, found by the address # each reordered pointer holds -- a cffi pointer hashes and compares # as that address. Handing back the array's own elements instead # would hand back pointers that own nothing, and that dangle the # moment the caller drops the sequence they point into owners = {pubkey: pubkey for pubkey in pubkeys} return [owners[pointer] for pointer in pointers]
[docs] def pubkey_sort( pubkeys_bytes: Sequence[BytesLike], compressed: bool = True ) -> list[bytes]: """Sort public keys, in lexicographic order of compressed form. This is the ordering of a BIP67 multisig script, and the one MuSig2 key aggregation applies when the participants have not agreed on a different one. Args: pubkeys_bytes: the public keys, each 33 or 65 bytes. An empty sequence sorts to an empty list. compressed: whether to return 33 bytes each rather than 65. Returns: The same keys, serialized, in ascending order. Raises: ValueError: if any key is not a valid point. RuntimeError: if libsecp256k1 fails to serialize, which no valid input can make it do. """ return [ serialize(pubkey, compressed) for pubkey in _pubkey_sort_([ parse(pubkey_bytes) for pubkey_bytes in pubkeys_bytes ]) ]
[docs] def parse(pubkey_bytes: BytesLike, name: str = "public key") -> CData: """Parse a public key into its internal libsecp256k1 representation. The internal form is what the raw `lib` calls take, and what `serialize` turns back into bytes: `serialize(parse(key))` is the compressed form of a key given in either form, which is `reserialize` and the reason it exists. It is also what every private half of this package takes, for which see the package docstring. Args: pubkey_bytes: the public key, 33 or 65 bytes. name: what the key is, as the exception should call it, for a caller passing more than one kind of public key -- `silentpayments` passes a scan public key, a spend public key, and one left to the default, and which one was refused is the whole of what its caller needs. Returns: The libsecp256k1 public key object. Raises: ValueError: if the bytes are not a valid point in either serialization. """ # spelled out rather than delegated to `_parsed`: the one python # frame between them is 0.010 microseconds of the 0.205 this call # costs -- an Apple M5, macOS 26.6, arm64, CPython 3.13.14, the two # spellings alternated in one process over 7 rounds of 400 000 calls, # minimum kept for each. Small, and it is the parse every wrapper # taking a public key begins with. `_parsed` answers None where this # raises, and `pubkey_verify` is what wants that pubkey_bytes = octets(pubkey_bytes, name) pubkey = ffi.new("secp256k1_pubkey *") if not lib.secp256k1_ec_pubkey_parse(ctx, pubkey, pubkey_bytes, len(pubkey_bytes)): raise ValueError(f"invalid {name}") return pubkey
def _parsed(pubkey_bytes: BytesLike, name: str) -> CData | None: """Parse a public key, answering None where it is not one. `secp256k1_ec_pubkey_parse` is the proof that octets are a public key, and what is done with the same proof differs: `parse` keeps what it built and raises when there is nothing to keep, `pubkey_verify` keeps nothing and answers the verdict. `pubkey_verify` is the only caller. `parse` spells the same three statements out rather than delegating, the frame between them being 0.027 microseconds of the 0.218 it costs -- an Apple M5, macOS 26.6, arm64, CPython 3.13.14, minimum of 9 rounds of 400 000 calls -- which is a tenth of the call, on the parse every wrapper taking a public key begins with. Two spellings of one parse is what that buys, and what holds them together is that either one refusing these octets is the other one refusing them: `tests/keys_test.py` asserts the pair over both serializations and over what is neither. Args: pubkey_bytes: the public key, 33 or 65 bytes. name: what the key is, as an exception should call it. Returns: The libsecp256k1 public key object, or None if libsecp256k1 refuses the octets -- a length no serialization has among them, which is why neither caller checks one. Raises: TypeError: if the value is not bytes, which is a malformed argument rather than a key that fails to parse: a caller with no octets at all asked no question about a key. """ pubkey_bytes = octets(pubkey_bytes, name) pubkey = ffi.new("secp256k1_pubkey *") if not lib.secp256k1_ec_pubkey_parse(ctx, pubkey, pubkey_bytes, len(pubkey_bytes)): return None return pubkey
[docs] def reserialize(pubkey_bytes: BytesLike, compressed: bool = True) -> bytes: """Prove octets a public key, and answer them in the form asked for. `serialize(parse(key))` as one call, which is two things a caller wants separately and always together. It is the **validation**: a library proving a key at its own boundary has `parse` and nothing to do with what `parse` returns, and this answers octets instead of an object whose lifetime is the caller's. And it is the **conversion**, which nothing else here offers: a caller holding an uncompressed key and needing the compressed one to hash has no other call to make, and one holding a compressed key and about to make several more calls with it has a reason to ask for the other form. The uncompressed serialization is the cheap one to open -- `parse` is 0.269 us on 65 bytes against 2.326 on 33, both coordinates being there to read where a compressed key is a field square root -- so `reserialize(key, compressed=False)` pays that root once and leaves every later call at the price of reading it. Args: pubkey_bytes: the public key, 33 or 65 bytes. compressed: whether to return 33 bytes rather than 65. Returns: The same point, serialized as asked. Raises: ValueError: if the bytes are not a valid point in either serialization. RuntimeError: if libsecp256k1 fails to serialize it, which no valid key can make it do. Example: >>> from btclib_secp256k1 import keys >>> compressed = keys.pubkey_from_prvkey(1) >>> uncompressed = keys.reserialize(compressed, compressed=False) >>> uncompressed == keys.pubkey_from_prvkey(1, compressed=False) True >>> keys.reserialize(uncompressed) == compressed True """ return serialize(parse(pubkey_bytes), compressed)
[docs] def serialize(pubkey: CData, compressed: bool = True) -> bytes: """Serialize an internal public key, in compressed form by default. Args: pubkey: the libsecp256k1 public key object, as `parse` returns. compressed: whether to return 33 bytes rather than 65. Returns: The 33-byte compressed serialization, or the 65-byte uncompressed one. Raises: RuntimeError: if libsecp256k1 refuses the object -- a NULL pointer, or a `secp256k1_pubkey` nothing has written to -- or fails for any other reason, which a key it produced cannot make it do. Which of the two it was is on the thread rather than in the message, and `context.check` is what reads it. Example: >>> from btclib_secp256k1 import keys >>> uncompressed = keys.pubkey_from_prvkey(1, compressed=False) >>> keys.serialize(keys.parse(uncompressed)).hex()[:10] '0279be667e' """ # the width and the buffer's type are declared together at the top of # this module, each type built from the width beside it, so the two # cannot say different numbers and neither is worked out again here. # The flag is decided in the branch that picks the buffer, so the one # condition is asked once. # # What is unpacked is the buffer, not the length libsecp256k1 reports # back: this serialization has one length per flag, so a buffer of # the wrong size has to reach the caller to be caught -- reading the # reported length instead would quietly accept an oversized one, # which a mutation session measured directly (`char[34]` survives # that spelling, and dies in this one). The DER serialization is the # other case, and reads `length[0]` for the reason given there. # # The length *object* is built per call and not hoisted, though it # holds the same number every time: libsecp256k1 writes 0 into it # before it does anything (`*outputlen = 0` in secp256k1.c) and # restores it only on success, and it holds the value it finds there # against 33 or 65 on the way in. One failed call would leave a # shared buffer at zero, and every later serialization -- on any # thread, of a perfectly good key -- would be refused if compressed: output, size, flags = ( ffi.new(_COMPRESSED_BUFFER_TYPE), _COMPRESSED_SIZE, COMPRESSED, ) else: output, size, flags = ( ffi.new(_UNCOMPRESSED_BUFFER_TYPE), _UNCOMPRESSED_SIZE, UNCOMPRESSED, ) length = ffi.new("size_t *", size) serialized = lib.secp256k1_ec_pubkey_serialize(ctx, output, length, pubkey, flags) if not serialized: msg = "point serialization failed" raise RuntimeError(msg) return ffi.unpack(output, size)