A guide by task¶
The rest of this documentation is the API reference: it answers “what does this function take”. This page answers the question before it, “which function do I call”, and it is arranged by what you want to do rather than by which module the code lives in.
Every example below is executed by btclib’s own test suite, in
tests/docs_examples_test.py. What follows a >>> prompt is what
the library answered, not what somebody expected it to answer, and an
example that stops being true fails a test rather than sitting here
misleading.
What btclib does, and what it will not do for you¶
btclib is a library about the cryptography and the encodings of bitcoin. Everything on this page happens in your process, offline. In particular btclib does not:
talk to the network. There is no node, no RPC client, no block explorer, no broadcasting. The last example on this page produces a fully signed transaction as hex; putting it on the chain is somebody else’s job (
bitcoin-cli sendrawtransaction, for instance)keep a wallet. Nothing is stored between runs, no key is written to disk, no address is remembered as “yours”, nothing tracks which of your outputs are unspent. When an example below needs the output being spent, the example supplies it
choose coins, estimate a fee, or pick a change amount. A fee is the difference between what the inputs are worth and what the outputs are worth, and that arithmetic is yours: btclib builds a transaction that pays a thousand-bitcoin fee without objection
protect your secrets from the machine they are on. Read the limitations section of SECURITY.md before you use it with a key that holds value: private keys live in ordinary, immutable python objects that are never zeroized, and — the least obvious limitation — not every operation reaches the constant-time C library. secp256k1 signing and generator multiplication are delegated to the libsecp256k1 bindings; another curve, another hash function, a nonce of your own, or a message that is not 32 bytes takes the pure python path instead, which is double-and-add and makes no attempt to be constant-time
Warning
Every private key, WIF and mnemonic on this page is a published test vector, copied from BIP39, BIP143 or BIP340 so that you can check btclib’s answer against the specification itself. They are known to the whole world. Never send bitcoin to an address derived from any of them, and never reuse one for anything real.
Installing¶
python -m pip install --upgrade "btclib[secp256k1]"
btclib requires python 3.10 or later. The secp256k1 extra pulls in
btclib-secp256k1, and it is the recommended install: it needs one of
that package’s wheels or a C toolchain to build it. Plain pip install
btclib works and installs no C at all — btclib then answers on its own
Python arithmetic, tens of times more slowly and not in constant time,
which SECURITY.md publishes.
secp256k1 arithmetic is delegated to those bindings, and the delegation
can be turned off. Setting BTCLIB_NO_LIBSECP256K1 to a non-empty
value in the environment, before btclib is imported, makes off the
state the process starts in:
BTCLIB_NO_LIBSECP256K1=1 python your_script.py
btclib.curves.set_libsecp256k1_serving() changes it from inside a
running process — including back on, so the variable sets the initial
state rather than locking one — and
btclib.curves.is_libsecp256k1_serving() reads the answer back.
Both are one state and not two: what they report is whether the next
call goes to libsecp256k1 or to the Python arithmetic, which is the only
difference a caller can act on.
The Python arithmetic is a second implementation and not a fallback of
convenience: it is slower — tens of times, and SECURITY.md publishes
that it is not constant-time — and it is what a project must run when it
has to check libsecp256k1 with something that is not libsecp256k1.
>>> import btclib
>>> btclib.__version__.startswith("20")
True
Which type to pass¶
Most of the public API takes “anything convertible” rather than one
type, so the rules of the conversion are worth learning first.
There are four aliases, all in btclib.alias, and one rule matters
more than the rest.
A ``str`` is hexadecimal, not text. Anywhere the signature says
Octets — a message to sign, a script, a hash — a str is parsed
with bytes.fromhex. Passing text where hex is expected fails:
>>> from btclib.ecc import dsa
>>> dsa.sign("hello world", 1)
Traceback (most recent call last):
btclib.exceptions.BTClibValueError: invalid hex string: non-hexadecimal number found in fromhex() arg at position 0
Pass bytes when you mean text, and let the hex spelling be for
things that are bytes:
>>> sig = dsa.sign(b"hello world", 1)
>>> type(sig).__name__
'Sig'
The four aliases:
Octets = bytes | str | bytearray | memoryviewBytes, or their hex spelling. Blanks inside the hex string are allowed, so
"02 cc71eb30..."is fine. Usebtclib.utils.bytes_from_octetsto normalize one yourself. The mutable buffers are here because every consumer takes one: amemoryviewsliced out of a larger field is octets like any other, and is not copied on its way in.String = bytes | str | bytearray | memoryviewThe same types, read the other way round: here a
stris text, and this is what addresses, WIFs and extended keys are. The alias a function names tells you which reading applies.PrvKeyAn
int, anOctetsscalar, a WIF, a BIP32xprv, or aBIP32KeyData. Prefer the WIF or the extended key. They carry the network and the compressed-public-key flag with them, sob58.p2pkh(wif)knows which address to build; a bare integer carries neither and everything downstream has to assume mainnet and compressed.This is the bitcoin layer’s type, not
ecc‘s. The signature schemes take a scalar — anint, itsn_sizeoctets, or their hex — because a WIF and an extended key areb58’s andbip32’s objects, and converting one is a call you make. Sob58.p2pkh(wif)works anddsa.sign(msg, wif)does not; passprv_keyinfo_from_prv_key(wif)[0].ecc.bmsis the exception, message signing being bitcoin: it still takes the lot.KeyA public key in any of its spellings — SEC bytes compressed or not, an
(x, y)tuple, anxpub— and also anything that is aPrvKey, from which the public key is computed. Convert once withbtclib.to_pub_key.pub_keyinfo_from_keyif you are about to use it repeatedly.
>>> from btclib import b58
>>> wif = b58.wif_from_prv_key(1)
>>> wif
'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn'
>>> b58.p2pkh(wif)
'1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'
>>> b58.p2pkh(1)
'1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'
>>> b58.wif_from_prv_key(1, compressed=False)
'5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf'
>>> b58.p2pkh(b58.wif_from_prv_key(1, compressed=False))
'1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm'
The same scalar, three addresses: the WIF is what remembers whether the public key was compressed, and the address follows from that.
A mnemonic, and the seed underneath it¶
A BIP39 mnemonic is a human-transcribable encoding of some entropy,
with a checksum so that a typo is caught. btclib.mnemonic.bip39
is the whole of it.
To make a fresh one, pass nothing and the operating system’s secrets
module supplies the entropy:
>>> from btclib.mnemonic import bip39
>>> words = bip39.mnemonic_from_entropy()
>>> len(words.split())
12
That one is genuinely random, so this page cannot show you its value. The rest of this section uses BIP39’s own first test vector — sixteen zero bytes — which is why you may recognize it:
>>> mnemonic = bip39.mnemonic_from_entropy(bytes.fromhex("00" * 16))
>>> mnemonic
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
Note the bytes.fromhex. mnemonic_from_entropy takes Entropy,
where a str is a binary 0/1 string rather than hex, so the
hex spelling has to be decoded first:
>>> bip39.entropy_from_mnemonic(mnemonic)
'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
128 bits, and they round-trip. More entropy means more words: 128 bits gives 12, 256 bits gives 24.
>>> len(bip39.mnemonic_from_entropy(bytes.fromhex("00" * 32)).split())
24
The checksum is what catches a backup written down wrong, and it is checked on the way back in:
>>> bip39.entropy_from_mnemonic(mnemonic.replace("about", "abandon"))
Traceback (most recent call last):
btclib.exceptions.BTClibValueError: invalid checksum: 0000; expected: 0011
From mnemonic to seed is PBKDF2 with 2048 iterations, salted with the
string "mnemonic" and the passphrase:
>>> seed = bip39.seed_from_mnemonic(mnemonic, "TREZOR")
>>> seed.hex()
'c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04'
That is byte for byte BIP39’s published seed for this mnemonic and this passphrase, which is the point of using a vector.
The passphrase is not a password on the mnemonic: it is an input to the derivation, so every passphrase yields a different, equally valid wallet, and there is nothing anywhere that can tell you a passphrase was wrong.
>>> bip39.seed_from_mnemonic(mnemonic, "").hex()[:32]
'5eb00bbddcf069084889a8ab91555681'
>>> bip39.seed_from_mnemonic(mnemonic, "TREZOR") == seed
True
The root extended private key is the seed run through BIP32, and
mxprv_from_mnemonic does both steps:
>>> rootxprv = bip39.mxprv_from_mnemonic(mnemonic, "TREZOR")
>>> rootxprv
'xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF'
btclib does not store that anywhere. It is a string in your process, and what happens to it next is entirely yours.
One backup behind many wallets¶
A second wallet means a second seed, and a second seed means a second
paper backup — which is the part of this that goes wrong. BIP85 removes
the second backup rather than the second wallet: a fully hardened path
off one root key derives the entropy another wallet is seeded from, so
what you keep is still one root key. btclib.bip85 is that
derivation and the formats the BIP defines on top of it.
The examples here use a key of the BIP’s own rather than the
rootxprv above, so the values below are the ones the specification
publishes:
>>> from btclib import bip85
>>> master = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
>>> bip85.mnemonic_from_root_key(master)
'girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose'
That is a BIP39 mnemonic like any other: type it into any wallet that reads one. What it is not is random — it is a function of the root key and the path, so it comes back the same forever, and the index is how you ask for the next one:
>>> bip85.mnemonic_from_root_key(master, index=1).split()[:3]
['mystery', 'car', 'occur']
The length and the language are path levels too, not formatting: another word count is another wallet, and so is another language.
>>> len(bip85.mnemonic_from_root_key(master, 24).split())
24
>>> bip85.mnemonic_from_root_key(master, 12, "it").split()[:2]
['smilzo', 'opinione']
A wallet that does not read BIP39 takes one of the other formats: the
WIF is what Bitcoin Core imports as an hdseed, taking the leading
256 bits as a private key, and the xprv is a BIP32 root of its own,
taking all 512 as a chain code and a key.
>>> bip85.wif_from_root_key(master)
'Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp'
>>> bip85.xprv_from_root_key(master)[:14]
'xprv9s21ZrQH14'
Not everything derived this way is a wallet. A password is a slice of the same entropy in base64 or base85, and dice rolls are drawn from a SHAKE256 stream seeded with it — useful where a die is what a wallet asks you for, and reproducible where a real die is not:
>>> bip85.base64_password_from_root_key(master, 21)
'dKLoepugzdVJvdL56ogNV'
>>> bip85.rolls_from_root_key(master, 10)
[1, 0, 0, 2, 0, 1, 5, 5, 2, 4]
For a path no function here formats, entropy_from_der_path is the
derivation itself and the truncation is yours:
>>> bip85.entropy_from_der_path(master, "m/83696968h/0h/0h").hex()[:32]
'efecfbccffea313214232d29e71563d9'
RSA is where the BIP stops rather than btclib: it says a key generator
should read BIP85’s own SHAKE256 stream and says nothing about how the
primes are found, so rsa_drng_from_root_key hands back that stream
and the key belongs to whatever library reads it.
What this costs is worth stating plainly. Every wallet derived here hangs off the one root key: back it up and you have backed up all of them; lose it and they go together, none of them being re-derivable from anything else. A passphrase on the BIP39 mnemonic underneath the root key is what changes the answer, because it changes the root key itself.
An account, its xpub, and the addresses under it¶
btclib.bip32.bip32 derives keys along a path. derive takes an
extended key and a path and hands back another extended key, as a
string:
>>> from btclib import bip32
>>> account = bip32.derive(rootxprv, "m/84h/0h/0h")
>>> xpub = bip32.xpub_from_xprv(account)
>>> xpub
'xpub6Crgkie5Rb7wDabkf4Uf6A2qnuERMA3p2QrnmHNQDrsXTaGvz9zugU38Apne8WqrcbSjdLwbhtfHrzWjNCJPVAkkNoQhMfzhBm8rKMA8KxH'
Paths are case-, blank- and slash-insensitive, and hardening may be
written h, H or '. The apostrophe is the one from the
specifications and the one that needs escaping in a shell, so h is
usually the easier spelling:
>>> bip32.derive(rootxprv, "m/84'/0'/0'") == account
True
>>> bip32.derive(rootxprv, [0x80000054, 0x80000000, 0x80000000]) == account
True
The account xpub is the useful object: it derives every receiving and change address of that account and cannot produce a single private key. That is what you copy onto the machine that watches the balance.
derive_from_account is the two remaining levels, and it refuses the
mistakes that make this dangerous — it insists the account key be
hardened, that the branch be 0 (receive) or 1 (change), and that
the index be sane:
>>> bip32.derive_from_account(xpub, 0, 0)
'xpub6FoBDgKSsn7RYSaiPAFqm476huu2RpAd2XswekGkmvNDRXvvvuqwSLXg1gvaSby83KTLrFNzXCp1gH4V2JJzMJryGNv7gE2Uk4kd8JXmvEF'
>>> bip32.derive_from_account(xpub, 2, 0)
Traceback (most recent call last):
btclib.exceptions.BTClibValueError: invalid branch number: 2 not in (0, 1)
The four address flavours¶
Which address a key becomes is a separate decision from where the key came from, and BIP44/49/84/86 are the convention that ties a purpose number to a script type. btclib does not enforce the tie — you pick the path, then you pick the address function — but following it is what lets another wallet find your coins from the same mnemonic.
>>> from btclib import b32, b58
>>> from btclib.script import taproot
>>> for purpose, address in [
... (44, lambda k: b58.p2pkh(k)),
... (49, lambda k: b58.p2wpkh_p2sh(k)),
... (84, lambda k: b32.p2wpkh(k)),
... (86, lambda k: b32.p2tr(taproot.output_pubkey(k)[0])),
... ]:
... acct = bip32.xpub_from_xprv(bip32.derive(rootxprv, f"m/{purpose}h/0h/0h"))
... print(purpose, address(bip32.derive_from_account(acct, 0, 0)))
44 1PEha8dk5Me5J1rZWpgqSt5F4BroTBLS5y
49 3Aho3kS7vgVWKTpRHjcqBoPXiCujiSuTaZ
84 bc1qv5rmq0kt9yz3pm36wvzct7p3x6mtgehjul0feu
86 bc1p3ryfth56dp058avv97ppn065ctsk263puvwp4rcka3wpg6cudp9qd3jsuu
The taproot one is the odd one out and deliberately so: a BIP86 address
is not the hash of the key but the key tweaked by a commitment to an
empty script tree, which taproot.output_pubkey computes. It returns
the 32-byte x-only output key and the parity bit that spending it needs.
SLIP-132 version bytes¶
Some wallets label an account key by script type, spelling it ypub
or zpub instead of xpub. That is SLIP-132, and it changes four
version bytes and nothing else. btclib.slip132 takes the
root key and does the derivation itself, because the version and the
path have to agree:
>>> from btclib import slip132
>>> bip32.xpub_from_xprv(slip132.p2wpkh_xkey(rootxprv))
'zpub6rXDN3yuixCtvAyzKn3uWLDr8qXKEQ2orduEL5AAysdHZmuPVUL2vbMQDEhp8L9hRsgM8J8idDNPdZjrob8R5e7x7UoYXVdfjDG96TLa7La'
>>> bip32.xpub_from_xprv(slip132.p2wpkh_p2sh_xkey(rootxprv))
'ypub6XRZqAj8R959SPF3UeP8gLsosvY7dskBv68N1rtd6pBwRwvAYJob7XS2VdxawoSdBdNEi32eQatwPBm412BZyfig481iqZKgseAZbdQdmMK'
Hand it something that is not a root key and it says so rather than guessing:
>>> slip132.p2wpkh_xkey(xpub)
Traceback (most recent call last):
btclib.exceptions.BTClibValueError: not a root key: depth 3, parent fingerprint 0xa40176dc
Addresses and the scripts behind them¶
An address is a script in disguise. btclib.script.script_pub_key.ScriptPubKey
is the thing itself, and it converts both ways:
>>> from btclib.script.script_pub_key import ScriptPubKey
>>> script_pub_key = ScriptPubKey.from_address(
... "bc1qv5rmq0kt9yz3pm36wvzct7p3x6mtgehjul0feu"
... )
>>> script_pub_key.script.hex()
'00146507b03ecb290510ee3a730585f83136b6b466f2'
>>> script_pub_key.type
'p2wpkh'
>>> script_pub_key.asm
['OP_0', '6507B03ECB290510EE3A730585F83136B6B466F2']
>>> script_pub_key.address
'bc1qv5rmq0kt9yz3pm36wvzct7p3x6mtgehjul0feu'
The network is carried by the object, so a testnet address decodes to a testnet script and encodes back to the same address.
Reading a raw transaction¶
btclib.tx.tx.Tx parses the wire format, hex or bytes. The one
below is BIP143’s worked example, unsigned:
>>> from btclib.tx import Tx
>>> raw = (
... "0100000002fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4"
... "e4ad969f0000000000eeffffffef51e1b804cc89d182d279655c3aa89e815b1b30"
... "9fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9"
... "148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976"
... "a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac11000000"
... )
>>> tx = Tx.parse(raw)
>>> tx.version
1
>>> tx.lock_time
17
>>> len(tx.vin), len(tx.vout)
(2, 2)
The identifier is a property, and it is reversed on the way out because that is how bitcoin displays a hash:
>>> tx.id.hex()
'3335ffae0df20c5407e8de12b49405c8e912371f00fe4132bfaf95ad49c40243'
>>> tx.size, tx.vsize, tx.weight
(160, 160, 640)
Watch the parentheses here. id, hash, size, vsize,
weight and vwitness are properties; is_segwit and
is_coinbase are methods and need calling. Forgetting the call gives
you a bound method, which is truthy, which is the sort of bug that
passes review:
>>> tx.is_segwit
False
An input names the output it spends. The transaction id inside
OutPoint is stored in wire order, so it reads back reversed from
what a block explorer shows:
>>> tx.vin[0].prev_out.tx_id.hex()
'9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff'
>>> tx.vin[0].prev_out.vout
0
>>> hex(tx.vin[0].sequence)
'0xffffffee'
An output is an amount in satoshi and a script, and the script knows its own address:
>>> tx.vout[0].value
112340000
>>> tx.vout[0].script_pub_key.type
'p2pkh'
>>> tx.vout[0].script_pub_key.address
'1Cu32FVupVCgHkMMRJdYJugxwo2Aprgk7H'
>>> tx.vout[1].script_pub_key.address
'16TZ8J6Q5iZKBWizWzFAYnrsaox5Z5aBRV'
Serialization is exact — what went in comes back out:
>>> tx.serialize(include_witness=True).hex() == raw
True
to_dict gives the shape bitcoin-cli decoderawtransaction gives,
which is handy for eyeballing:
>>> sorted(tx.to_dict())
['hash', 'locktime', 'size', 'txid', 'version', 'vin', 'vout', 'vsize', 'weight']
Building a transaction¶
A transaction is four fields, and btclib does not hide any of them. Amounts are integers in satoshi throughout: there is no float anywhere in the library, on purpose.
>>> from btclib.tx import OutPoint, TxIn, TxOut
>>> funding = OutPoint(
... bytes.fromhex(
... "8ac60eb9575db5b2d987e29f301b5b819ea83a5c6579d282d189cc04b8e151ef"
... ),
... 1,
... )
>>> tx_in = TxIn(funding, b"", 0xFFFFFFFF)
>>> tx_out = TxOut.from_address(599000000, "bc1qr583w2swedy2acd7rung055k8t3n7udp7vyzyg")
>>> unsigned = Tx(1, 0, [tx_in], [tx_out])
>>> unsigned.serialize(include_witness=True).hex()
'0100000001ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff01c003b423000000001600141d0f172a0ecb48aee1be1f2687d2963ae33f71a100000000'
There is no fee field. The fee is what the inputs are worth minus what the outputs are worth, and btclib cannot compute the first half — the value of an input lives in the previous transaction, which is not in this one. You have to supply it:
>>> prevout = TxOut.from_address(600000000, "bc1qr583w2swedy2acd7rung055k8t3n7udp7vyzyg")
>>> prevout.value - unsigned.vout[0].value
1000000
The hash that gets signed¶
A signature does not sign the transaction; it signs a hash derived from it, and which hash depends on the script type of the output being spent, on the sighash flags, and — for segwit — on the amount. Getting this wrong is the classic way to produce a signature that verifies against nothing.
btclib.script.sig_hash.from_tx() is the function to call: give it
the outputs being spent, the transaction, which input, and the hash
type, and it dispatches to the legacy, segwit v0 or taproot rule for
you.
>>> from btclib.script import sig_hash
>>> sig_hash.from_tx([prevout], unsigned, 0, 0x01).hex()
'8feec313e988999f1db646742b8a287269d10706cd086f8cda79aa491cc843af'
Check that it agrees with the specification. BIP143 publishes both the transaction and the hash for its native-p2wpkh example, so btclib’s answer has something to be right about:
>>> from btclib.script.script_pub_key import ScriptPubKey
>>> bip143_prevouts = [
... TxOut(
... 625000000,
... ScriptPubKey(
... "2103c9f4836b9a4f77fc0d81f7bcb01b7f1b35916864b9476c241ce9fc198bd25432ac"
... ),
... ),
... TxOut(600000000, ScriptPubKey("00141d0f172a0ecb48aee1be1f2687d2963ae33f71a1")),
... ]
>>> sig_hash.from_tx(bip143_prevouts, tx, 1, 0x01).hex()
'c37af31116d1b27caf68aae9e3ac82f1477929014d5b917657d0eb49478cb670'
That is BIP143’s published sigHash. The hash type 0x01 is
SIGHASH_ALL; btclib.script.sig_hash names the others
(NONE, SINGLE, ANYONECANPAY).
Signing, and checking the result without a node¶
The private key below is BIP143’s, published in the specification.
>>> prv_key = "619c335025c7f4012e556c2a58b2506e30b8511b53ade95ea316fd8c3286feb9"
>>> from btclib.to_pub_key import pub_keyinfo_from_prv_key
>>> pub_key = pub_keyinfo_from_prv_key(prv_key)[0]
>>> pub_key.hex()
'025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357'
>>> b32.p2wpkh(prv_key)
'bc1qr583w2swedy2acd7rung055k8t3n7udp7vyzyg'
Sign the hash from the previous section. Note sign_, with the
trailing underscore: throughout btclib that suffix means “the caller has
already reduced the input with the hash function”. dsa.sign would
hash its argument again, which is not what you want here — the sighash
is the message.
>>> from btclib.ecc import dsa
>>> msg_hash = sig_hash.from_tx([prevout], unsigned, 0, 0x01)
>>> sig = dsa.sign_(msg_hash, prv_key)
>>> sig.serialize().hex()
'304402207b7dffe084afa0d951726827d8469628e646349e9e6e1d60b76aa91d4a459ff2022003c3fd5ed4795126862efa7bc194d03c76af29741eb36c47659ac39506f2de10'
The nonce is RFC-6979 deterministic, the s value is the canonical
low one and the r is ground low as Bitcoin Core grinds it — 70 bytes
of DER rather than 71 — so this signature is the same on every machine
and every run. btclib never invents randomness for an ECDSA signature.
A segwit v0 input is spent with a witness, not with a script_sig: the DER signature with the hash type appended, then the public key.
>>> from btclib.script.witness import Witness
>>> signed = Tx(1, 0, [TxIn(funding, b"", 0xFFFFFFFF)], [tx_out])
>>> signed.vin[0].script_witness = Witness([sig.serialize() + b"\x01", pub_key])
>>> signed.is_segwit
True
>>> signed.serialize(include_witness=True).hex()
'01000000000101ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff01c003b423000000001600141d0f172a0ecb48aee1be1f2687d2963ae33f71a10247304402207b7dffe084afa0d951726827d8469628e646349e9e6e1d60b76aa91d4a459ff2022003c3fd5ed4795126862efa7bc194d03c76af29741eb36c47659ac39506f2de100121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635700000000'
Now the two identifiers differ, which is what segwit bought: the witness is not covered by the txid.
>>> signed.id.hex()
'4ebbcec1c2b21740e5e4ffe6aaffe485f6285e7afcbce3130cf34cee05d653c7'
>>> signed.hash.hex()
'3f0a8a5f75909c79c075e7f62333ad7ae2cf05112283225a67202fa6deec4a19'
>>> signed.id == unsigned.id
True
>>> signed.size, signed.vsize, signed.weight
(191, 110, 437)
Finally, do not take your own word for it. btclib ships a script
interpreter, so the transaction can be verified against the outputs it
spends before it goes anywhere near a node:
btclib.script.engine.verify_transaction() raises on a bad one and
returns None on a good one.
>>> from btclib.script.engine import verify_transaction
>>> verify_transaction([prevout], signed)
Change one satoshi of the output and the signature no longer covers it.
TxOut is a frozen dataclass, so tampering means building another one
rather than assigning to a field — which is the library saying that an
output you have already signed over is not yours to edit:
>>> tampered = Tx(1, 0, [TxIn(funding, b"", 0xFFFFFFFF)], [TxOut.from_address(
... 599000001, "bc1qr583w2swedy2acd7rung055k8t3n7udp7vyzyg"
... )])
>>> tampered.vin[0].script_witness = signed.vin[0].script_witness
>>> verify_transaction([prevout], tampered)
Traceback (most recent call last):
btclib.exceptions.BTClibValueError: false top stack element
Signatures on their own¶
The two schemes live in btclib.ecc.dsa (ECDSA, what pre-taproot
bitcoin uses) and btclib.ecc.ssa (BIP340 Schnorr, what taproot
uses). Both come in two spellings, and the difference is the trailing
underscore: sign hashes its argument for you, sign_ takes the
hash.
ECDSA¶
>>> from btclib.ecc import dsa
>>> sig = dsa.sign(b"Satoshi Nakamoto", prv_key)
>>> sig.serialize().hex()
'304402204729dbf89f9288d56d32d1aea04db19df37c45c0a02863602f9ad98e7e506ce4022017489be41857144bf5782e964632d254b6b13ea22ce84837f775540784341f43'
>>> dsa.verify(b"Satoshi Nakamoto", pub_key, sig)
True
>>> dsa.verify(b"satoshi nakamoto", pub_key, sig)
False
Verification returns a bool and never raises, which is what you want in
a loop over untrusted input; dsa.assert_as_valid is the spelling
that raises with a reason, for when you want to know why.
A verifier that has the signature but not the public key can recover it,
which is what makes the 65-byte message signature below shorter than a
key plus a signature. Recovery answers up to four candidate keys, so the
signer has to say which one is theirs: dsa.sign_recoverable is
dsa.sign with that number beside it — the key_id
dsa.recover_pub_key takes. It costs nothing to ask for: the two bits
are computed by any signer and thrown away by sign.
>>> sig, key_id = dsa.sign_recoverable(b"Satoshi Nakamoto", prv_key)
>>> key_id
1
>>> from btclib.curves import bytes_from_point
>>> recovered = dsa.recover_pub_key(key_id, b"Satoshi Nakamoto", sig)
>>> bytes_from_point(recovered) == pub_key
True
BIP340 Schnorr¶
BIP340 keys are x-only: 32 bytes, with the y coordinate implied even.
gen_keys returns the scalar and that x coordinate.
>>> from btclib.ecc import ssa
>>> q, x_Q = ssa.gen_keys(
... "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF"
... )
>>> x_Q.to_bytes(32, "big").hex().upper()
'DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659'
Unlike ECDSA, a BIP340 nonce mixes in auxiliary randomness, so
ssa.sign is not deterministic unless you supply the aux
argument. Supplying it is what makes the example below reproducible, and
it is exactly what BIP340’s test vector 1 does:
>>> aux = "0000000000000000000000000000000000000000000000000000000000000001"
>>> msg = "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89"
>>> sig = ssa.sign_(msg, "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF", aux)
>>> sig.serialize().hex().upper()
'6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A'
>>> ssa.verify_(msg, x_Q, sig)
True
That is BIP340’s published signature for vector 1. In production leave
aux alone and let secrets fill it: the randomness is a defence
against fault attacks, and the signature is valid either way.
Signing a message with an address¶
btclib.ecc.bms is the “sign this text with the key behind this
address” scheme that wallets expose. It prefixes the message with the
magic string "Bitcoin Signed Message:\n", signs the hash with ECDSA,
and serializes the result as 65 fixed bytes — a recovery flag, then
r, then s — base64-encoded. The public key is not transmitted:
verification recovers it from the signature and checks that it hashes to
the address.
The message is Octets, so text has to be bytes. It is signed
byte for byte, with no trimming of your own.
>>> from btclib.ecc import bms
>>> wif = b58.wif_from_prv_key(prv_key)
>>> address = b58.p2pkh(wif)
>>> address
'13eeg4y5wYGxNTxBEuWLPFauoMJQLxdoip'
>>> signature = bms.sign(b"paid invoice 42 on 2026-01-01", wif)
>>> signature.b64encode()
'IPn1BZ4xi86uCS5NrSrX+2g+RSsHgm94QX5+krikMl4Fd25te6nrF53iI9amQ5XtAcM4fMggHPybYOxufoR8MVA='
>>> bms.verify(b"paid invoice 42 on 2026-01-01", address, signature)
True
>>> bms.verify(b"paid invoice 43 on 2026-01-01", address, signature)
False
The recovery flag encodes both which of the recovered public keys is the
right one and what kind of address it is; 31 to 34 mean a
compressed p2pkh key, which Electrum also accepts for p2wpkh and
p2wpkh-p2sh.
>>> signature.rf
32
The whole point of the scheme is proving control of an address, so sign
something that cannot be replayed out of context: a date, a
counterparty, and what the statement is for. The module docstring of
btclib.ecc.bms says the same at more length, and it is worth
reading before you use this for anything that matters.
Partially signed transactions¶
A PSBT (BIP174) is the container that lets an unsigned transaction, the
data needed to sign it, and the signatures themselves travel between
programs that do not trust each other — a watch-only wallet, a hardware
signer, a coordinator. BIP174 describes the work as roles, and
btclib.psbt.psbt gives you the data structure for each of them,
not a wallet that plays them.
Creator. From an unsigned transaction:
>>> from btclib.psbt import Psbt
>>> psbt = Psbt.from_tx(unsigned)
>>> psbt.b64encode()
'cHNidP8BAFIBAAAAAe9R4bgEzInRgtJ5ZVw6qJ6BWxswn+KH2bK1XVe5DsaKAQAAAAD/////AcADtCMAAAAAFgAUHQ8XKg7LSK7hvh8mh9KWOuM/caEAAAAAAAAA'
>>> len(psbt.inputs), len(psbt.outputs)
(1, 1)
Updater. A signer cannot compute a segwit sighash without the amount being spent, and the amount is not in the transaction, so the updater puts it in:
>>> psbt.inputs[0].witness_utxo = prevout
>>> psbt.inputs[0].witness_utxo.value
600000000
Signer. btclib does not sign a PSBT for you — it has no idea which
keys are yours. What it hands you is the message: ecdsa_sig_hash
reads the utxo and the scripts off the psbt, which is where a psbt keeps
them, and returns the hash the input signs. The signature then goes in
the slot BIP174 defines for it, keyed by public key, with the hash type
appended:
>>> from btclib.psbt import ecdsa_sig_hash
>>> msg_hash = ecdsa_sig_hash(psbt, 0)
>>> der = dsa.sign_(msg_hash, prv_key).serialize()
>>> psbt.inputs[0].partial_sigs = {pub_key: der + b"\x01"}
>>> psbt.assert_signable()
The hash type is the one the input asks for, SIGHASH_ALL when it
asks for none, and a keyword argument when the signer chooses.
sig_hash.from_tx above cannot serve here: it reads the redeem script
out of an input’s script_sig and the witness script off its witness
stack, and an unsigned transaction has neither — in a psbt they are
fields. A taproot input signs taproot_sig_hash instead, its
signature being schnorr and travelling in the taproot fields rather than
in partial_sigs.
A PSBT survives base64 round-tripping, which is how it moves between programs:
>>> Psbt.b64decode(psbt.b64encode()) == psbt
True
Version 2 (BIP370). In version 2 the unsigned transaction stops being a field: the transaction version, each input’s outpoint and sequence, and each output’s amount and script live in the psbt itself, which is what lets a Constructor add inputs and outputs after the psbt exists. btclib holds every psbt that way and computes the transaction from it, so the two versions differ only in how they are written, and converting is a method each way:
>>> v2 = psbt.to_v2()
>>> v2.version, v2.tx_version, v2.lock_time
(2, 1, 0)
>>> v2.inputs[0].previous_tx_id == psbt.tx.vin[0].prev_out.tx_id
True
>>> v2.tx == psbt.tx
True
>>> v2.to_v0() == psbt
True
psbt.tx is computed from the fields at every access, so writing into
it writes into a copy: to change what is being built, set the field —
psbt.inputs[0].sequence, psbt.outputs[0].amount.
A version 2 psbt says what may still be changed, in
PSBT_GLOBAL_TX_MODIFIABLE, and btclib honours it: sort_inputs,
sort_outputs and join refuse to reorder or add on a side
the flags do not allow, since every signature already made commits to
the order it saw:
>>> v2.tx_modifiable = 0
>>> v2.sort_inputs()
Traceback (most recent call last):
btclib.exceptions.BTClibValueError: the inputs are not modifiable
The lock time of a version 2 psbt is computed too, from the lock times
its inputs require and the fallback for when none does — and a psbt
whose inputs require both a block height and a timestamp has no lock
time at all, one nLockTime being one number of one kind. That is a
psbt btclib refuses rather than resolves.
Finalizer and Extractor. finalize turns the partial
signatures into a final script_sig or witness and extract_tx pulls
out the network transaction. Be aware of the shape they handle: they
build what BIP174’s own vectors need, which are p2sh and p2wsh
multisig, and they do not know that a bare p2wpkh input wants its
signature in the witness rather than in the script_sig. For a
single-key segwit input, build the witness yourself as the previous
section does — two stack items, signature then public key — and use
verify_transaction to check the result.
A psbt too large to hold. Everything above reads the whole psbt into
memory first, which for a psbt whose inputs each carry the previous
transaction they spend is all of them at once. PsbtView reads the same
psbt out of a seekable stream — a file as much as a BytesIO — and
keeps no map at all: it learns where each one begins and reads one when
it is asked for it.
>>> from io import BytesIO
>>> from btclib.psbt import PsbtView
>>> view = PsbtView(BytesIO(psbt.serialize()))
>>> view.input_count, view.output_count
(1, 1)
>>> view.input(0).witness_utxo.value
600000000
>>> view.ecdsa_sig_hash(0) == msg_hash
True
It is a reader: the stream is never written, so a signer assembles its own
answer out of the maps the view hands it, one PsbtIn per input signed.
>>> from btclib.psbt import PsbtIn
>>> psbt_in = view.input(0)
>>> psbt_in.partial_sigs = {pub_key: der + b"\x01"}
>>> answer = PsbtIn.parse(psbt_in.serialize())
>>> answer.partial_sigs == psbt.inputs[0].partial_sigs
True
What it holds between calls is in the module docstring of
btclib.psbt.psbt_view, and so is the one rule using it imposes:
the stream must not change while the view is alive. A view answers from
the bytes that were there when it read them, and an amount or a script
that changes between two reads is a sighash committing to a transaction
the signer was never shown — so a psbt on removable or untrusted storage
is copied into memory you control before it is viewed.
Where to go next¶
the README lists every feature, and its module layout table is the fastest way to guess where something lives
SECURITY.md has the limitations in full; read it before trusting any of this with value
the API reference under PYTHON PACKAGE documents every module. The docstrings carry the reasoning, not just the signatures
the test suite is the second half of the documentation.
tests/mirrorssrc/btclib/and reproduces the published vectors of the BIPs, of RFC 6979 and of Bitcoin Core, so if you want to know exactly what btclib promises about something, the test for it is where it is written down