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. With the bindings installed, what you pass decides whether a given call does. A curve other than secp256k1 takes the pure python path whatever the call; on secp256k1 an operation may carry a further condition of its own, and that section gives them. The hash function is one of those conditions rather than a second absolute: where an operation has one,
sha256itself is what counts andfunctools.partial(sha256)is not it; where an operation has none, hashing with something else leaves the call delegated. The pure python path is double-and-add and makes no attempt to be constant-time
Warning
Every private key and WIF on this page is either a published test vector, copied from BIP143 or BIP340 so that you can check btclib’s answer against the specification itself, or a scalar as small as 1. 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.11 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_ECC_NO_LIBSECP256K1 to a
non-empty value in the environment, before btclib is imported, makes
off the state the process starts in:
BTCLIB_ECC_NO_LIBSECP256K1=1 python your_script.py
btclib_ecc.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_ecc.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.ecc import dsa
>>> dsa.sign("hello world", 1)
Traceback (most recent call last):
btclib_ecc.exceptions.BTClibEccValueError: 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. That coercion copies it, so writing to a buffer after passing it does not reach what btclib built from it.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.Integer = Octets | intAn
int, or anOctetsscalar: what a private key is spelled as. Neither spelling carries a network or a compressed-public-key flag, so everything downstream is told mainnet and compressed, or is told otherwise by an argument.The two spellings that do carry both are read where they are defined (issue #1188). A WIF is
b58’s own object,b58.prv_key_data_from_wifreading it andb58.wif_from_prv_keybuilding it; an extended key isbtclib_wallet.bip32’s, andbtclib_wallet.bip32.prv_keyinfo_from_xprvanswers the same(scalar, network, compressed)triple from anxprv. An address builder takes neither spelling: it takes the public key, whichb58.prv_key_data_from_wif(wif).pubandbtclib_wallet.bip32.pub_keyinfo_from_xkey(xkey)are the two ways to reach.The arithmetic layer reads it with
btclib_ecc.curves.scalar_from_prv_key, and a second name for the same union of types would be nothing a type checker could tell apart (issue #1188). It does not take an extended key:dsa.sign(msg, xprv)does not work; passbtclib_wallet.bip32.prv_keyinfo_from_xprv(xprv)[0].ecc.bmsis narrower still: message signing wants the network and the compression too, so it takes thebtclib.key.PrvKeyDataa WIF and a scalar both resolve to —b58.prv_key_data_from_wif(wif)for the one,btclib.key.PrvKeyData(q)for the other.btclib.key.PubKeyDataA public key parsed once and carried: the SEC octets, compressed or not, and the network they are read on. It is what
b58.p2pkh,b58.p2wpkh_p2sh,b32.p2wpkhand theScriptPubKeyconstructors take, and a caller says which half of a key pair it holds rather than leaving the size and the format to decide (issue #1188).btclib.key.PrvKeyData(q).pubderives it from a scalar,b58.prv_key_data_from_wif(wif).pubfrom a WIF, andbtclib_wallet.bip32.pub_keyinfo_from_xkey(xkey)answers the pair aPubKeyDatais built from for an extended key.
>>> from btclib import b58
>>> from btclib.key import PrvKeyData
>>> wif = b58.wif_from_prv_key(1)
>>> wif
'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn'
>>> b58.p2pkh(b58.prv_key_data_from_wif(wif).pub)
'1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'
>>> b58.p2pkh(PrvKeyData(1).pub)
'1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'
>>> b58.wif_from_prv_key(1, compressed=False)
'5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf'
>>> b58.p2pkh(PrvKeyData(1, compressed=False).pub)
'1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm'
The same scalar, two addresses: compressed is what decides which
public key it derives, and the address follows from that. A WIF carries
that flag, which is why prv_key_data_from_wif answers it.
Four address flavours¶
Which address a key becomes is a decision separate from the key itself:
one public key has a p2pkh, a p2wpkh-p2sh, a p2wpkh and a p2tr address,
and the address function is what picks. BIP44, BIP49, BIP84 and BIP86
tie the purpose of a derivation path to one of them, and that convention
is btclib_wallet.bip44’s; here the key is given directly.
>>> from btclib import b32
>>> from btclib.script import taproot
>>> pub = PrvKeyData(1).pub
>>> for name, address in [
... ("p2pkh", lambda k: b58.p2pkh(k)),
... ("p2wpkh-p2sh", lambda k: b58.p2wpkh_p2sh(k)),
... ("p2wpkh", lambda k: b32.p2wpkh(k)),
... ("p2tr", lambda k: b32.p2tr(taproot.output_pubkey(k)[0])),
... ]:
... print(name, address(pub))
p2pkh 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
p2wpkh-p2sh 3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN
p2wpkh bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
p2tr bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9
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.
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_ecc.curves import scalar_from_prv_key
>>> key = PrvKeyData(scalar_from_prv_key(prv_key))
>>> pub_key = key.pub.sec
>>> pub_key.hex()
'025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357'
>>> b32.p2wpkh(key.pub)
'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.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.ScriptError: false top stack element
The refusal is a ScriptError, and its code
is the error Bitcoin Core gives the same spend, with Core’s own words for it:
>>> from btclib.exceptions import ScriptError
>>> try:
... verify_transaction([prevout], tampered)
... except ScriptError as error:
... print(error.code.name, "--", error.code.description)
EVAL_FALSE -- Script evaluated without error but finished with a false/empty top stack element
Signatures on their own¶
The two schemes live in btclib_ecc.ecc.dsa (ECDSA, what pre-taproot
bitcoin uses) and btclib_ecc.ecc.ssa (BIP340 Schnorr, what taproot
uses), in the btclib-ecc distribution that btclib depends on. 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.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_ecc.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.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(b58.prv_key_data_from_wif(wif).pub)
>>> address
'13eeg4y5wYGxNTxBEuWLPFauoMJQLxdoip'
>>> signature = bms.sign(
... b"paid invoice 42 on 2026-01-01", b58.prv_key_data_from_wif(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.
Where to go next¶
the README lists every feature, and ARCHITECTURE 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