A Python library for 'bitcoin cryptography'
btclib is a Python3 type annotated library for teaching, learning and using bitcoin, focused on elliptic curve cryptography and bitcoin’s blockchain. It started as a teaching tool for Ferdinando Ametrano’s Bitcoin and Blockchain Technology course, it is used in production today (still marked as beta because it is often refactored for improved clarity).
The test suite covers virtually the whole code base, a floor the build
enforces, and it answers to vectors their authors publish: the BIPs’ and
the SLIPs’ own, Bitcoin Core’s script, transaction, sighash and
key-encoding files, HWI’s, trezor’s for BIP39 and SLIP39, and Appendix A.2
of RFC 6979. tests/_data/README.md pins each vendored file to the
upstream commit it was copied from, and says whether the two still match —
including the few vectors that are btclib’s own, having no upstream.
The library is not limited to secp256k1, and for that curve it always calls btclib_libsecp256k1, FFI bindings to Bitcoin Core’s optimized C library libsecp256k1. They are a required dependency and not an optional accelerator, so installing btclib needs one of their wheels or a C toolchain. The Python arithmetic serves every other curve, and the suite validates it against the bindings: libsecp256k1 says what the right answer is, being what bitcoin consensus relies on.
Included features are:
m/purpose'/coin_type'/account'/change/address_index path, the purpose
selecting the encoding: 44 p2pkh, 49 p2wpkh-p2sh, 84 p2wpkh (BIP84),
86 p2tr (BIP86)wsh() and as a tr() leaf:
the expression compiled to a script, a script read back into the
expression it is, the type system that says an expression is well formed,
the bounds a spend of it is analysed by, and the non-malleable witness
that satisfies itbitcoin: payment URIssign(address, msg)
needs — an output descriptor per chain, and a script template with
multisig quorums in it, for the pre-descriptor wallets no descriptor
states. Each answers address(branch, index),
script_pub_key(branch, index) and position_of(script_pub_key), the
last being “is this output mine”, compared whole and never on a key
origin’s fingerprintbtclib is used to teach and to prototype as much as to build, and the two uses want different things of it. What follows is the boundary between them, before a private key is handed to any of the above.
A Python object carrying secret material cannot be reliably zeroized: it stays in the process memory until garbage collection, and the interpreter may have copied it meanwhile. The constant-time properties are libsecp256k1’s, and they hold on the C side of the call — not before it, and not after.
Not every operation crosses that call. dsa.sign and ssa.sign reach the
bindings for secp256k1 with sha256 and no nonce of the caller’s; another
curve, another hash function, or a nonce you supply runs the Python
arithmetic instead, which the suite validates against the bindings but
which is not constant-time. So a caller whose threat model includes timing
should stay on the delegated paths, or keep the key out of the process
altogether: btclib.hwi drives a hardware wallet through HWI, behind the
same PsbtSigner contract a software signer answers.
SECURITY’s “Limitations, not vulnerabilities” states each condition exactly — which arguments delegate, which do not, and what the Python path does hide — and is the canonical text; this section is the pointer to it.
Three pairs of modules are one idea split in two, and each split runs one way only:
| the codec / the arithmetic | the bitcoin semantics on top |
|---|---|
btclib.curves — Curve, mult |
btclib.ecc — dsa, ssa, bms |
btclib.base58 — the encoding |
btclib.b58 — WIF, p2pkh, p2sh |
btclib.bech32 — the encoding |
btclib.b32 — p2wpkh, p2wsh, p2tr |
The right column imports the left one; the left never imports the right.
So from btclib.ecc import dsa for a signature,
from btclib.curves import mult for a point multiplication, btclib.b58
for an address, btclib.base58 for the encoding on its own. Each of the
six modules says the same in its own docstring.
The rest, roughly bottom-up. alias holds the types the public API
accepts, much of it taking anything convertible rather than one type, and
exceptions the errors it raises. to_prv_key and to_pub_key accept
any key representation and hand back one. bip32 and mnemonic derive
keys. script, tx, block and psbt build and validate what goes on
the chain, and script.engine runs a transaction against the consensus
rules.
Above them, bip44 composes bip32, script.taproot and both address
encodings into an address from an extended key and a derivation path, and
descriptors reads the BIP380 grammar and hands back the scripts a
descriptor names – with descriptors.miniscript reading BIP379’s
language, which is a script written as a tree of fragments, and
satisfying one. psbt_signer
is the contract an external signer answers; hwi is that contract over
Bitcoin Core’s HWI.
Nothing in the library imports bip21, bip322, slip132, fee,
wallet, hwi or fetch: they are the top of the stack, and fetch
is the only one that goes out to the network. wallet remembers which
addresses it has handed out — over bip44, over descriptors or over a
script template of its own — and its key wallets sign for one with
ecc.bms.
bip322 is the other message signing, and it is at the top rather than
beside ecc.bms because it needs everything below it: a script, a
transaction, a psbt and the engine that runs them.
The rpc client fetch speaks through is not in that stack: it is
bitcoin-core-rpc, a
package of its own that btclib depends on — one file, standard library
only, installable or copyable, and usable by anyone who wants a node
client and no bitcoin library. btclib.fetch turns its answers into Tx
and TxOut, and checks the chain the node reports against the network
those are labelled for.
The dependency stops at btclib/fetch/. bitcoin-core-rpc declares its own
FetchError, importing nothing of btclib’s being what lets its file be
vendored, and btclib.fetch.fetcher.client_errors re-raises it as
btclib.exceptions’ own, with the status and the code carried across:
an except FetchError written against btclib catches what a fetcher
raises, and no module outside that package loads urllib.request.
Constructing a client opens no socket; the first call does.
To install, or upgrade:
python -m pip install --upgrade btclib
In a virtual environment:
python -m venv venv_btclib
source venv_btclib/bin/activate
python -m pip install --upgrade btclib
On Windows the second line is venv_btclib\Scripts\activate in CMD and
PowerShell, source venv_btclib/Scripts/activate in Git bash.
CONTRIBUTING is for development, SECURITY for reporting a vulnerability.