Metadata-Version: 2.4
Name: bitcurve
Version: 0.0.1
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Classifier: Topic :: Security :: Cryptography
License-File: LICENSE
Summary: Fast libsecp256k1 bindings for Python with a coincurve compatible API.
Keywords: secp256k1,bitcoin,ecdsa,schnorr,bip340,cryptography
Author-email: dni <office@dnilabs.com>
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# bitcurve

Fast [libsecp256k1](https://github.com/bitcoin-core/secp256k1) bindings for Python with a
[coincurve](https://github.com/ofek/coincurve) compatible API.

- ECDSA (RFC6979) signing and verification, recoverable signatures, BIP340 Schnorr signatures,
  x-only keys, ECDH, key tweaking, DER/PEM private keys.
- Written in Rust with [PyO3](https://pyo3.rs) on top of the
  [`secp256k1`](https://github.com/rust-bitcoin/rust-secp256k1) crate, which bundles bitcoin-core's
  libsecp256k1.
- Built against the stable Python ABI (abi3): **one wheel per platform works on Python 3.10, 3.15 and
  every later version**, there is nothing to rebuild when a new Python comes out.
- No runtime dependencies.

## Why bitcurve

coincurve is the library most of the Python Bitcoin and Lightning ecosystem uses for secp256k1,
including [lnurl](https://github.com/lnbits/lnurl), [bolt11](https://github.com/lnbits/bolt11) and
[LNbits](https://github.com/lnbits/lnbits). Its last release, 21.0.0, is from **2025-03-08**, more
than a year ago, and it is now breaking installs:

- coincurve 21.0.0 ships wheels only up to Python 3.13 and its source build fails against current
  cffi, so it can not be installed on Python 3.14 or 3.15.
- coincurve 20.0.0 no longer builds at all with scikit-build-core 1.0.
- The fixes for Python 3.14 are merged upstream (ofek/coincurve#225, 2026-08-08), but have not been
  released, see the requests in ofek/coincurve#219.

bitcurve exists so these projects keep working on every Python version without waiting for a
coincurve release. Because the wheels use the stable ABI, the same problem can not happen again with
the next Python release.

## Install

```console
pip install bitcurve
```

## Usage

```python
from bitcurve import PrivateKey, PublicKey, PublicKeyXOnly, verify_signature

key = PrivateKey()
signature = key.sign(b"hello")
assert key.public_key.verify(signature, b"hello")
assert verify_signature(signature, b"hello", key.public_key.format())

# recoverable signatures, as used by BOLT11 invoices
recoverable = key.sign_recoverable(b"hello")
assert PublicKey.from_signature_and_message(recoverable, b"hello") == key.public_key

# BIP340 Schnorr signatures
digest = bytes(32)
schnorr = key.sign_schnorr(digest)
assert key.public_key_xonly.verify(schnorr, digest)
assert PublicKeyXOnly(key.public_key_xonly.format()).verify(schnorr, digest)
```

## Migrating from coincurve

bitcurve implements the public API of coincurve 21: `PrivateKey`, `PublicKey`, `PublicKeyXOnly`,
`verify_signature`, `Context`/`GLOBAL_CONTEXT` and the helpers in `coincurve.ecdsa`, with the same
signatures, defaults and error messages. For most code, changing the import is all there is to do:

```diff
-from coincurve import PrivateKey, PublicKey
-from coincurve.ecdsa import cdata_to_der, deserialize_recoverable, recoverable_convert
+from bitcurve import PrivateKey, PublicKey
+from bitcurve.ecdsa import cdata_to_der, deserialize_recoverable, recoverable_convert
```

The outputs are byte for byte identical, which the test suite checks against coincurve (see
[Tests](#tests)). The differences:

- `custom_nonce` is not supported, signatures always use RFC6979 nonces. Passing anything but the
  default raises `ValueError`.
- `Context` is accepted everywhere for compatibility but carries no state, bitcurve uses one
  randomized libsecp256k1 context per thread.
- Where coincurve passes cffi structs, bitcurve passes bytes or opaque objects: `PublicKey.public_key`
  is a parsed `RawPublicKey`, and the `bitcurve.ecdsa` helpers take and return signatures as bytes
  (64 byte compact or 65 byte recoverable). Chains like
  `cdata_to_der(recoverable_convert(deserialize_recoverable(sig)))` work unchanged.
- `PrivateKey.public_key` and `PrivateKey.public_key_xonly` are derived on first access instead of
  in the constructor.

## Benchmarks

`benchmarks/bench.py` runs the same operations with coincurve and bitcurve on the shared test
vectors in `tests/vectors/` (64 keys and messages, plus the official BIP340 vectors). Before timing
anything it checks that both libraries return identical results, then it reports the median time
per operation.

```console
uv sync
uv run maturin develop --release --uv
uv run python benchmarks/bench.py              # table
uv run python benchmarks/bench.py --markdown   # markdown table, as below
uv run python benchmarks/bench.py --repeat 15 --json results.json
```

Build with `--release` first, a debug build is several times slower. coincurve only installs on
Python 3.13 and older, so that is where the comparison can run. CI runs the benchmark on every push
and writes the table to the job summary.

Results on an Intel Core i7-4790K @ 4.00GHz, Python 3.13.13, coincurve 21.0.0, median of 15 runs.
Speedup above 1 means bitcurve is faster:

| Operation | coincurve | bitcurve | Speedup |
|---|---:|---:|---:|
| PrivateKey + public key | 48.60 µs | 53.86 µs | 0.90x |
| PublicKey parse | 6.74 µs | 4.93 µs | 1.37x |
| ECDSA sign | 33.71 µs | 62.14 µs | 0.54x |
| ECDSA verify | 42.18 µs | 42.10 µs | 1.00x |
| verify_signature | 47.33 µs | 47.16 µs | 1.00x |
| ECDSA sign recoverable | 33.43 µs | 62.15 µs | 0.54x |
| ECDSA recover public key | 51.16 µs | 49.00 µs | 1.04x |
| bolt11 signature check | 49.76 µs | 48.18 µs | 1.03x |
| Schnorr sign | 87.83 µs | 148.37 µs | 0.59x |
| Schnorr verify | 41.73 µs | 42.56 µs | 0.98x |
| Schnorr verify (BIP340) | 40.60 µs | 41.05 µs | 0.99x |
| ECDH | 52.91 µs | 51.74 µs | 1.02x |
| PublicKey tweak add | 32.34 µs | 30.99 µs | 1.04x |
| PrivateKey to DER | 4.97 µs | 4.31 µs | 1.15x |

Everything that only uses public data (verification, recovery, parsing, ECDH and tweaks) is as fast
as coincurve or slightly faster.

### Why signing is about 1.8x slower

Signing is slower on purpose. Both libraries run the same libsecp256k1 code for the signature
itself, the difference is what happens afterwards.

libsecp256k1 protects secret keys against side-channel attacks by blinding its computations with a
random value stored in the context. coincurve randomizes its context once, when it is created. The
`secp256k1` crate goes further: after every operation that touches a secret key it rerandomizes the
context, mixing in fresh randomness and the secret itself, so the blinding never stays the same for
long. That costs about one extra elliptic curve multiplication, roughly 28 µs on the machine above:

- ECDSA signing (normal and recoverable) rerandomizes once: about 62 µs instead of 34 µs, 1.8x.
- Schnorr signing rerandomizes twice, once when the keypair is created and once when signing:
  about 148 µs instead of 88 µs, 1.7x.
- Creating a key rerandomizes once as well, but bitcurve only derives the public key that is
  actually used while coincurve computes both the normal and the x-only public key up front, so the
  difference is only 10%.

For the workloads bitcurve is written for, like signing an invoice or an LNURL-auth challenge, 28 µs
per signature does not matter, the extra side-channel protection does. Verification, which servers do
far more of, is not affected.

## Development

Requires a Rust toolchain and [uv](https://docs.astral.sh/uv/).

```console
uv sync                                 # builds the extension in debug mode
uv run pytest
uv run maturin develop --release --uv   # rebuild after changing src/lib.rs
cargo fmt --check && cargo clippy -- -D warnings
uv run ruff check . && uv run black --check . && uv run mypy
```

### Tests

- `tests/test_vectors.py` checks bitcurve against `tests/vectors/coincurve.json`, every output
  coincurve 21.0.0 produced for 64 deterministic keys and messages, and against the official BIP340
  test vectors. It runs on every Python version, coincurve is not needed.
- `tests/test_coincurve_compat.py` compares bitcurve with an installed coincurve on random inputs and
  checks that bitcurve has every public name coincurve has. Skipped where coincurve is not installed.
- `tests/test_keys.py` covers error handling and edge cases.

`scripts/generate_vectors.py` regenerates `tests/vectors/coincurve.json`.

## Releasing

Push a `vMAJOR.MINOR.PATCH` tag:

```console
git tag v0.1.0
git push origin v0.1.0
```

The version only lives in the tag, `Cargo.toml` carries `0.0.0`. The release workflow writes the
tag's version into `Cargo.toml` (`scripts/stamp_version.py`), builds abi3 wheels for Linux
(manylinux and musllinux, x86_64 and aarch64), macOS (x86_64 and arm64) and Windows (x64 and arm64)
plus the sdist, tests every wheel it can run, refuses to publish files that do not carry the tag's
version, uploads to PyPI with trusted publishing and creates a GitHub release.

PyPI trusted publishing has to be configured once for the repository and `release.yml`.

## Credits

bitcurve would not exist without [coincurve](https://github.com/ofek/coincurve) by
[Ofek Lev](https://github.com/ofek). Its API, documentation and behaviour are the model for
bitcurve, and coincurve served as the reference implementation for the test vectors. Thank you for
maintaining it for so many years. coincurve is licensed under MIT or Apache-2.0.

bitcurve is built on [bitcoin-core/secp256k1](https://github.com/bitcoin-core/secp256k1) (MIT), the
[rust-bitcoin/rust-secp256k1](https://github.com/rust-bitcoin/rust-secp256k1) crate (CC0-1.0) and
[PyO3](https://github.com/PyO3/pyo3) (MIT or Apache-2.0). The BIP340 test vectors in
`tests/vectors/bip340.csv` are from [bitcoin/bips](https://github.com/bitcoin/bips/tree/master/bip-0340)
(BSD-2-Clause, MIT or CC0-1.0).

## License

MIT

