Releases: btclib-org/btclib-libsecp256k1
Release list
v0.7.1.3
The same
libsecp256k1 0.7.1
(1a53f49) as v0.7.1.2: no breaking changes.
- A dynamic wheel's import failure says why.
ImportErrornow names
every shared-object candidate_load_librejected and chains the last
loader error, instead of only "no loadable shared libsecp256k1 found". cryptographymoved to 50.0.0, closing CVE-2026-69247 — a PKCS#7
decryption oracle pulled in transitively through the packaging tools,
and unreachable from this package's own code either way.- Two README clarifications: why
pubkey_from_prvkeyis two
libsecp256k1 calls rather than one, and which btclib functions guard a
MuSig2 nonce against reuse.
v0.7.1.2
The same
libsecp256k1 0.7.1
(1a53f49) as v0.7.1: no breaking changes — nothing in the public API
moved, and the one wrapper whose behaviour changed changed the text of an
error message. What a fourth number carries here is one new function, the
contract of every function where a caller reads it, a static extension
compiled the way the interpreter compiles its own, and metadata that says
what the wheels are.
keys.pubkey_from_prvkeyis the public key of a private key,
compressed by default, and the 65 bytes ofmult.mult_with
compressed=False— which is whatmult_now is. Generator
multiplication was the one producer here that could not answer the
compressed form, so a caller who wanted the 33 bytes had to parse and
re-serialize what libsecp256k1 had just serialized, or slice the parity
out of the 65 bytes themselves.mult_answers exactly what it did;
the one thing to know is that theValueErrorof a scalar outside
[1, n-1]now names a private key, which is what
secp256k1_ec_pubkey_createcalls its argument, so code matching on
that message matches on the new text.- Every function documents its arguments, its return value and what it
raises. That contract used to be prose in the README, several hundred
lines from the functions it applies to, which for a package shipped as
a compiled wheel is all a reader ofhelp()or of an IDE tooltip
could have had.mult.mult_andmult.mult, which shared a docstring
verbatim while returning 65 uncompressed bytes and a pair of ints, now
each say which. Apydoclinthook keeps it that way. - The README opens with a Quickstart — sign and verify, ECDSA and
BIP340 — and every example in it, and in every docstring, is executed
by the suite on each interpreter and each kind of wheel. An example
that stops working fails a build instead of sitting there. - The static extension is faster on Unix, and correct on universal2.
It was compiled without the interpreter's ownCFLAGS: no
optimization at all, unlike everything CMake builds beside it, and on
a universal2 interpreter a single-arch object inside a dual-arch
bundle. - A wheel whose extensions disagree on static or dynamic is refused
rather than reported. It would have been taggedpy3-none-<platform>
while holding an ABI-specific extension: installable on any
interpreter of that platform, and broken on most of them. - The classifiers say what the package is: the three systems the
wheels are built for instead ofOS Independent, andTyping :: Typed, which is the classifier PyPI users filter on. - More of the library is held to published vectors: BIP324 for
ellswift encoding and theellswift_xdhshared secret, BIP327 for
every step of MuSig2 aggregation, the four long BIP340 messages
throughssa.sign_custom, and recovery ids 2 and 3, which no test had
ever reached. - Two hooks that were passing without reading a file now read one.
The copyright notice hook was checking only newly added staged files,
so on a clean checkout — every run in CI — it checked nothing; the
entropy detectors ofdetect-secretswere off for the whole tree
rather than for the vector files that motivated it. - The release path checks before it builds what it used to discover
after uploading: that the tag is onmaster, and that this file has
a section for it. A rehearsal re-run gets a version of its own instead
of colliding with itself on TestPyPI, and no longer shares a
concurrency group with a push to the branch it was dispatched from.
v0.7.1.1
The same
libsecp256k1 0.7.1
(1a53f49) as v0.7.1, and the same bindings: no wrapper changed
behaviour, and nothing in the public API moved. What a fourth number
carries here is the floor the package is built on, its documentation,
and the gate around both.
- Dropped support for Python 3.9 (minimum is now 3.10): it reached end
of life in October 2025, and keeping it helduv.lockat a second
resolution of its own, cffi 2.1 requiring 3.10 and cffi 2.0 staying
pinned for 3.9 alone. It also cost the build matrix its widest cell,
CPython having no Windows arm64 build before 3.11 - Published documentation: a Sphinx build under
docs/, served at
Read the Docs, which the
documentationmetadata url now names instead of the README - The distribution names The btclib developers as its author and ships
AUTHORS.mdbesideLICENSEandCOPYRIGHT. The licence header of
every source file is the short MIT one, so the three no longer state
the same holder and the same terms three different ways - Three scheduled workflows join
published, each asking on a morning
of its own a question no pull request can: whether the urls in the
prose still resolve (links), whether the tree survives every
dependency at its newest (latest), and whether the suite would
notice a wrong line (mutation). None of them gates a merge, and that
is the design: what they report is the outside world moving, or a test
nobody has written, and neither is something a branch can act on - Each vendored test vector is pinned to a commit and a blob SHA-1 in
tests/README.md, so a citation names the revision actually copied
rather than a file that changes underneath it, and a monthly workflow
opens an issue when upstream moves one - New
REPOSITORY.md: the branch rules, required checks, token
permissions, publishing environments and scanning settings that live
outside the tree and cannot be recovered by reading it, each with the
API call that reads it back - The pre-commit gate, which is the one definition of clean and what CI
runs rather than a second list of its own, now also measures the yaml,
the prose and the packaging metadata, and checks itself. mypy runs
with more of its optional error codes and ruff with more rule-sets,
every one of which the tree already satisfied - More tests, and what each one verifies is now stated in its docstring
and gated as such: the callbacks, the copyright headers, the
documentation build and the vendored data are covered where they were
not
v0.7.1
Major changes include:
- Wrapped
libsecp256k1 0.7.1
(1a53f49) - Wrapped the
ecdh,recovery,ellswift(BIP324) andmusig
(MuSig2) libsecp256k1 modules, besides the already wrapped
extrakeysandschnorrsigones; newecdh,recovery, and
ellswiftbinding modules, while MuSig2 is available through the raw
cffi bindings only, by decision: its two-round protocol needs a
session whose secret nonce cannot be reused, which is a property of an
object's lifetime and belongs where the signing state lives, in
btclib; what has no state in it is wrapped here (key ordering and
aggregation, taproot tweaking, tagged hashing, and the BIP340
verification an aggregate signature reduces to) - New
keysbinding module: private and public key algebra (verify,
negate, tweak add and multiply, combine), including the
multiplication of an arbitrary point, whichmultdoes not provide - New
xonlybinding module: BIP341 taproot tweaking of x-only public
keys (tweak_add,tweak_add_check) and of the corresponding
private keys, so that a key path spending can be signed withssa - New
hashesbinding module: the BIP340 tagged hash
(tagged_sha256), which the taproot tags of BIP341 (TapLeaf,
TapBranch, TapTweak) and the BIP340 challenge are built on keysalso provides the lexicographic ordering of public keys
(pubkey_cmp,pubkey_sort), which is the one of a BIP67 multisig
script and the one MuSig2 key aggregation applies by default- New
ssa.sign_custom: BIP340 signing of a message of any length,
whichsigncannot do andverifycould already check dsanow exposes the signature malleability primitives
(normalize,is_low_s) and the conversions between the DER and the
64-byte compact encodings (to_compact,to_der)dsa.verifyandssa.verifyreturnboolinstead ofintssa.verifytakes the 32-byte x-only public key BIP340 verifies
against, and so doxonly.tweak_addandxonly.tweak_add_check: a
full public key used to be accepted and reinterpreted as its even y
point, which is a different key from the one passed whenever y is odd.
xonly.from_pubkeyis that conversion, and making it is the caller's
decision- the 32 bytes of nonce entropy are 32 bytes or omitted:
ndata
(dsa.sign,recovery.sign),aux_rand32(ssa.sign,
ssa.sign_custom,ellswift.create) andrnd32(ellswift.encode)
used to be left padded when shorter, turning a caller mistake into a
valid argument; a wrong length now raisesValueError. The boundary
checks what C cannot see and normalizes nothing else, which the README
states as such under What the boundary checks - All the wrapped modules are now requested explicitly at configure
time (recovery, in particular, is disabled by default upstream):
upstream defaults are not part of its API, and the autotools and
CMake build paths were enabling different module sets - The vendored library is now built with CMake on every platform, in a
single build path: autotools (on POSIX) and mingw cross-compilation
(through--host) are gone, and with them the need for automake,
libtool, pkg-config and autoconf. CMake is declared as a build
requirement, so that installing the sdist provisions it instead of
demanding a package manager step: only a C toolchain is needed - The build no longer writes inside the vendored tree: the callback
stubs are added to the library target from the CMake binary
directory, which is outside the submodule, sogit reset --hardand
git clean -fxdon the submodule (which used to discard any local
change to it) are gone, and a wheel built on Windows can no longer
ship the CMake build tree in an sdist - Fixed the shared object lookup of a dynamic build giving up on the
first candidate directory, which the CMake layout (POSIX libraries in
lib, Windows DLLs inbin) would have hit - New versioning scheme: release numbers now track the wrapped
libsecp256k1 version; binding-only releases append a fourth number
(0.7.1.1, 0.7.1.2, etc.) - Added support for Python 3.13 and 3.14
- Dropped support for Python 3.7 and 3.8 (minimum is now 3.9):
both are end-of-life (3.7 since June 2023, 3.8 since October 2024)
and the current build/test infrastructure cannot support them anymore,
as cibuildwheel 4.x does not build cp37/cp38 wheels and GitHub-hosted
runners no longer provide those interpreters - Added macOS arm64 and Linux aarch64 wheels
- Added native Windows wheels for CPython, built with CMake/MSVC via
cibuildwheel, for both x86_64 and arm64 (win_arm64from CPython 3.11,
the first version with a Windows arm64 build); the mingw
cross-compiled dynamic wheel is still provided, x86_64 only - Dynamic wheels now carry the platform tag of the interpreter they are
built with (it was hardcoded to x86_64, with a fake macOS 10.16
minimum version) - The vendored library is now built for the architecture of the
interpreter, which is the one its toolchain compiles the extension
for, instead of the one CMake infers from the host: an emulated
interpreter (an x86-64 CPython on Windows arm64, which is what uv
installs there by default) used to get an archive of the wrong
architecture, and every libsecp256k1 symbol unresolved at link time - Updated CI to current GitHub Actions runners and actions,
cibuildwheel 4.x (static wheels now cp39-cp314, PyPy opt-in) - Hardened CI: least-privilege GITHUB_TOKEN (contents: read), actions
pinned to commit SHAs, superseded runs auto-cancelled - New release workflow: tag-triggered, publishing to PyPI with Trusted
Publishing (OIDC, no long-lived tokens) and PEP 740 attestations,
behind a manual approval gate - New
SECURITY.md: how to report a vulnerability, and where the
boundary with libsecp256k1 lies, as the cryptography is upstream and
what is ours is the layer driving it - Updated all pre-commit hooks to their latest versions
- Replaced black, isort, flake8, autoflake, pyupgrade, bandit,
pydocstringformatter and yesqa with ruff: one linter and formatter,
one configuration section - mypy now runs in strict mode; the cffi extension module is described
by a hand-written stub (stubs/_btclib_libsecp256k1.pyi), without
whichffiandlibareAnyand the whole package type-checks
vacuously. Opaque libsecp256k1 handles are spelledCDatain the
public signatures that return them - The package now exposes
__version__ - CI additionally gates on the pre-commit hooks, on branch coverage
with afail_underratchet, on installing from the sdist (the only
path that compiles libsecp256k1 on the user's machine), and on the
twine check --strictandcheck-wheel-contentsvalidation of every
wheel and sdist before a release can reach PyPI - Development environment managed by uv,
with PEP 735 dependency groups and the interpreter pinned in
.python-version; the hatch environments and the noxfile are gone - PEP 639 license metadata (
license = "MIT"pluslicense-files,
replacing the deprecated license table and theLicense ::
classifier) - An import failure of the extension now raises
ImportErrornaming
the directory searched, instead of a bareNameError - Fixed sdist builds with multi-pass PEP 517 frontends such as uv:
the callback stubs are now a separate compilation unit,
no longer mutating the vendored sources (#20) - Build scripts now fail fast on subprocess errors
- Tests now cover the official BIP340 test vectors, the published
RFC6979 deterministic ECDSA vectors for secp256k1, the secp256k1-py
ECDSA vectors used by btclib, DER edge cases, and the error paths
of the bindings: every line and branch reachable through the API is
covered, thefail_underratchet is at 100%, and the unreachable
RuntimeErrorpaths are excluded from the measure instead of being
counted as a gap that could never be closed - New invariant tests, over inputs derived from a SHA256 chain rather
than chosen: round trips, the scalar operations agreeing with the
point ones, and the two sides of ECDH and of BIP324 agreeing. They
reach what a fixed key cannot, such as a public key whose x
coordinate starts with a zero byte or a signature whose DER encoding
is 69 bytes, both pinned as well; no test dependency is added, the
suite running inside every wheel test environment too - Bindings now validate inputs and check libsecp256k1 return codes,
raisingValueErrorwith a clear message; malformed keys and
signatures were previously verified against uninitialized memory - A single shared libsecp256k1 context is created with the modern
SECP256K1_CONTEXT_NONEflag (the SIGN/VERIFY flags are deprecated)
and randomized to protect against side-channel leakage - The shared context now records what libsecp256k1 reports through its
illegal argument and internal error callbacks, and the new
context.check()raises it, the failed precondition verbatim, as a
ValueErroror aRuntimeError. The do-nothing stubs of the vendored
build keep an illegal argument from aborting the process, but left the
caller a bare 0 and no reason for it; that is unreachable through the
bindings, which validate their arguments first, and matters for a call
made throughlib, as a MuSig2 session is: signing twice with the
same secret nonce is refused by libsecp256k1 through that very
callback. What is recorded is per thread, as the callback runs on the
thread of the call that triggered it - The bindings are documented as safe to call concurrently, and tested
for it: the shared context is only mutated at import time, and a
wheel is built for the free-threaded interpreter (cp314t), where
those calls are no longer serialized
v0.4.0
Major changes include:
- Wrapped
libsecp256k1 0.4.0
(199d27c)
v0.3.0
Major changes include:
- Wrapped libsecp256k1 0.3.2(acf5c55)
- Build platform wheels using cibuildwheel
- Switched from setuptools to hatch
- Improved project standards (pyproject.toml, nox)
v0.2.1
Major changes include:
- Fixed bug in
mult
v0.2.0
Major changes include:
- Wrapped libsecp256k1 0.2.0(21ffe4b)
- Increased test coverage
- Improved project standards (pre-commit hooks, mypy, tox)
v0.1.1
Major changes include:
- Fixed
multreturn type
v0.1
Major changes include:
- Wrapped libsecp256k1(3efeb9da)
- Updated nonce generation
- Added
multmodule