Changelog¶
A release’s own pull request writes the release’s section, from the squash subjects since the previous tag, and no other pull request adds an entry (sections 9 and 12 of the organization’s standard). An entry is for anything a reader would notice, and is narrower than “every change”: a comment reworded inside a workflow changes nothing a reader of this repository meets. RELEASE_NOTES.md has the release notes, which say what a user has to act on; this file is the record behind them.
Neither file states how many entries it holds: a stated number is one more line to edit at every release.
v2026.11.13¶
bip44 discovers an account’s used addresses from public keys¶
discover_account and discover_accounts scan each branch through a
fetch.HistoryReader up to the gap limit, as BIP44 does (closes #816).
A ledger holds its file from load to close¶
A second ledger, a restore or another SQLite connection on the file is
refused, as Core holds a wallet’s (closes #800); an open error naming no
SQLite code is refused, not raised as AttributeError (closes #870).
A key let go mid-sign is refused as locked¶
sign calls a ledger locked, not damaged, where another thread locks it
while it decrypts a source’s keys (closes #801).
A tapscript’s duplicate keys are compared by their x¶
Parsing refuses a miniscript tr() leaf naming one x under two parities (closes #824).
A history server’s list keeps only what touches a script¶
ElectrumFetcher.get_history and EsploraFetcher.get_history drop a
listed transaction that pays or spends none of the scripts (closes #703)
(closes #704) (closes #709).
Electrum’s history headers chain to the tip¶
ElectrumFetcher.get_history refuses a confirmation, or a start, whose header
no chain to the server’s tip holds (closes #708) (closes #873).
A psbt’s text is bounded before it is decoded¶
Psbt.b64decode and tx_or_psbt_from_any refuse a base64 or hex text
over the size of a 16 MiB psbt (closes #823).
finalize refuses a key or a spend Core leaves incomplete¶
A single key whose hash the script does not hold, and an uncompressed p2wpkh key, are refused; p2sh-p2pkh carries its redeem script (closes #799) (closes #830) (closes #831).
A p2sh-wrapped p2tr program is no taproot input¶
sign, finalize, the amount check and estimated_input_sizes read it as
a witness program of no known rule (closes #804) (closes #818).
A MuSig2 or FROST session refuses a non-taproot input¶
Or one whose control block does not prove its leaf, before round 1 (closes #820).
BIP322 bounds a signature’s text and its to_sign’s weight¶
b64decode refuses a text over its variant’s bound, and assert_as_valid
a to_sign over MAX_BLOCK_WEIGHT (closes #744) (closes #826).
A refusal describes the value it refuses, or names it¶
The fetchers, electrum’s errors and the descriptor parser print no value the caller passed but a CompactFilterFetcher’s height of its chain, nor an Electrum server’s text (closes #802) (closes #805) (closes #806).
The fetchers’ bounds and codes are described, not printed¶
max_blocks, max_pages, an RPC code and an HTTP path print no caller’s
value (closes #828) (closes #836) (closes #837) (closes #843).
speculos runs its arm build under qemu-user¶
(closes #825).
qa-assets’ btclib output is named by its locked tag¶
(closes #784).
AGENTS.md carries the organization’s code review rules¶
(issue btclib-org/.github#2009).
The bestpractices.dev answers are re-measured at v2026.11.12¶
(closes #803) (issue #784, btclib-org/.github#2205, btclib-org/.github#2214).
The dev tooling moves to newer releases¶
Dependabot’s coverage bump (#844); soupsieve 4.1, past GHSA-mmhr-3c5m-f64f, in the docs group (closes #875).
v2026.11.12¶
A ledger encrypts its private keys at rest¶
Ledger.encrypt and its unlock, lock and passphrase change, as Core’s
do. The file’s format is 2, migrating from 1 on load, and cryptography
is a dependency except on Windows arm64 (issue #717).
A ledger’s connection reads the row of each pragma it sets¶
Unread, journal_mode’s statement kept every commit from finishing on
PyPy (closes #810).
A ledger bumps a transaction’s fee¶
Ledger.bump_psbt and Ledger.bump, as Core’s psbtbumpfee and
bumpfee do. Spends take Core’s BIP125 sequence unless
replaceable=False, and carry comments (issue #715).
An ECDSA sig_hash is refused for a witness program with no rule¶
ecdsa_sig_hash raises for such a psbt input, and sign skips it, as
Core does (closes #747).
A refusal describes the value it refuses by its type and size¶
Their texts change, electrum’s, psbt’s and silent_payments’s
among them (closes #745, #749, #790, #791, #792, #793, #795).
PsbtOut.parse refuses a PSBT_OUT_TAP_TREE key with key data¶
As BIP-174 and Core do (closes #754).
An unchecked invoice’s unknown network is a BTClibValueError¶
Bolt11Invoice.to_invoice and payee raised a bare KeyError
(closes #770).
The suite accepts both base58-bech32 texts for a refused 5-bit word¶
(closes #808).
The bestpractices.dev answers are held in .bestpractices.json¶
(issue btclib-org/.github#2153, issue btclib-org/.github#2159).
The actions, the hooks and the dev tooling move to newer releases¶
Dependabot’s and pre-commit’s bumps (#751, #753, #778).
v2026.11.11¶
ecc 2026.11.8 and btclib 2026.11.10 are the floors¶
The schemes come from ecc’s top level and bms from btclib.bms. The
lock holds btclib 2026.11.10, ecc 2026.11.11 and btclib-secp256k1
0.8.0.14 (closes #729, #720, #731, #735).
The Base58Check and bech32 codecs come from base58-bech32¶
A direct dependency, at 2026.10.23 or later. A refusal of theirs that a
caller’s input reaches leaves as BTClibValueError or
BTClibTypeError (issue #731).
btclib-secp256k1 0.8.0.13 is the floor of the secp256k1 extra¶
The first release whose silentpayments.scan_outputs refuses a label
tweak outside [1, n-1] (issue #735).
bip322.sign takes a WIF¶
As btclib.bms.sign does (closes #733).
combine and join AND the undefined tx-modifiable bits¶
As Core’s combinepsbt does (closes #686).
Esplora’s history read is bounded¶
EsploraFetcher.get_history reads at most max_pages pages a script,
and skips a mempool transaction gone mid-read (closes #699).
RelayFeeReader and start_height_for_time¶
A node’s relay fee floors, and the height a wallet born at a given time has history from (closes #711).
btclib_wallet.ledger keeps descriptor wallets’ transactions and coins¶
A Ledger syncs from a HistoryReader, answers by Core’s wallet rules,
spends, marks conflicts, abandons, keeps labels, backs up and restores
(issue #712, #713, #714, #716, #642).
PSBT_IN_POR_COMMITMENT stays in unknown¶
No code changes: a test pins it, as Core keeps it (closes #622).
Core is cited at the release integration-bitcoind.yml pins¶
(issue #645). Two hardened children of one xprv are an agreement with Core (closes #718), and the history reorg test runs on a regtest node of its own (closes #722).
The intersphinx inventories map the siblings’ stable docs¶
(closes #723).
deps-latest, deps-oldest, links and os-windows fixed on main¶
(issue #735). HWI and Speculos install without the project’s uv configuration (issue #735).
The README’s status badges sit in a Status section at its end¶
(issue btclib-org/.github#1856).
The tree names the organization’s standard as section 11 asks¶
The shared files take btclib-org/.github’s citation of the standard (issue btclib-org/.github#1856, issue btclib-org/.github#2137).
v2026.11.7¶
Esplora, Electrum and Core’s REST list a script’s history¶
EsploraFetcher, ElectrumFetcher and BitcoinCoreRestFetcher satisfy
HistoryReader, and btclib_wallet.electrum reads
blockchain.scripthash.get_history (closes #629).
bip322 refuses a non-minimal OP_IF or OP_NOTIF in legacy and P2SH¶
REQUIRED_RULES adds btclib’s ScriptFlag.MINIMALIF_ALL, and the btclib
floor moves to 2026.10.30 (closes #493).
BIP375’s k follows the spend key¶
Within one scan key, outputs sort by spend key, then by output index, the rule bitcoin/bips#2207 merged (closes #676).
Every silent-payment address must belong to its recipient¶
output_keys and the psbt.silent_payments module say why, and what to
do where an address cannot be checked (closes #685).
The homepage is https://btclib.org/btclib-wallet/¶
The documentation stays on Read the Docs (closes #644).
README.md links its own files absolutely, so they resolve on PyPI¶
The documentation keeps them as links to its own pages (issue btclib-org/.github#1851).
pytest-timeout bounds each test¶
At 300 s, so a hang names its test (closes #650).
Sourcery runs on every pull request¶
Its findings on main are fixed, or skipped by name with the reason
beside the line (issue btclib-org/.github#2005).
The calls of btclib-org/.github name the commit of its tag v2026.11.5¶
CodeQL runs the security-extended queries (issue btclib-org/.github#2005).
check-executables-have-shebangs runs¶
(issue btclib-org/btclib#2852).
.coderabbit.yaml configures CodeRabbit’s review¶
(issue btclib-org/.github#1998).
The lock, the fuzz build and the workflows take Dependabot’s updates¶
The lock takes bitcoin-core-rpc 2026.10.23, hypothesis 6.170.13, mypy
2.4.1, pytest 9.2.0, ruff 0.17.3 and uv-build 0.13.6, .clusterfuzzlite
the same bitcoin-core-rpc and uv-build, and the workflows setup-uv 10.3.3.
v2026.11.3¶
The silent-payment output key is computed without tweak_add_var¶
output_key computes t_k*G with mult and adds B_m with curves.add,
and output_keys calls it in place of the bindings’ create_outputs.
The ecc floor moves to 2026.11.2 (issue #591).
A test holds silent payments to curves.mult for t_k*G¶
Tests only (closes #656).
The silent-payment label address is computed with mult and add¶
In labeled_address_from_keys, as in output_key. _scan says why the
scan may stay variable time (closes #640).
bip39-slip39 2026.11.2 is the floor¶
So a pre-2.0 Electrum mnemonic whose hex seed has an odd number of characters gives a key (closes #655).
check_vendored_vectors.py prints a failed gh call’s stderr¶
And retries a server error. RELEASING.md says how to find the dependents in place of listing them (closes #654, #660, #639).
macOS runs on arm64 alone¶
os-macos.yml and pypi-install.yml run no x86_64 macOS image (issue
btclib-org/.github#1982).
RELEASING.md names the lint jobs a release run skips¶
(issue btclib-org/.github#1989).
The calls of btclib-org/.github name the commit of its tag v2026.11.2¶
public-api and api-break run api_gaps.py beside griffe, and docs
runs check_included_links.py (issue btclib-org/.github#1988). The
pre-commit hooks take ruff 0.17.3, pydoclint 0.12.0 and uv 0.13.6.
v2026.11.2¶
The wallet declares its own errors and the Electrum codec¶
As btclib_wallet.exceptions and btclib_wallet.electrum. The
integration-electrs and -floresta workflows run on a change to either
(issue btclib-org/btclib#2709).
CompactFilterFetcher cross-checks filter headers across peers¶
The peers of cross_check, at cfcheckpt and cfheaders, as BIP157’s
Client Operation recommends. transport is a read-only property
(closes #352).
HistoryReader lists the transactions touching some scripts¶
A protocol of btclib_wallet.fetch, which BitcoinCoreFetcher implements
over RPC. scan_blocks refuses an empty script (issue #629).
scan_transaction_outputs scans in Python for few labels¶
With at most three labels, or at least ten outputs per label, and with the bindings otherwise, where they serve. Both scans refuse a malformed label (issue #474).
parse reads a tr() branch’s subtrees before its count¶
As Core does (closes #608).
The psbt interop test compares a v2 PSBT field by field¶
By decodepsbt’s names at Core v32.1, which decodes a v2 PSBT. A refusal
from Core counts as a disagreement. Tests only (closes #617).
rust-psbt’s vectors are read from each case’s task, as base64¶
The files under tests/psbt/_data/rust-psbt/ are rust-psbt 24a45463’s.
Tests only (closes #544).
prv_keys_contract_test walks only the package’s own routines¶
So it passes on Python 3.11 and 3.12. Tests only (issue #647).
v2026.10.31¶
update_psbt_input and update_psbt_output are functions of psbt¶
They take the descriptor as their first argument, in place of a method on
every descriptor class. Descriptor.expansion returns the fields they
write (issue #620).
miniscript_sizer, satisfaction_sizer and miniscript_solver move to psbt¶
From descriptors (issue #620).
account_descriptors moves from descriptors to bip44¶
descriptors no longer imports bip44 (issue #620).
The wallet imports no private name of bip32 or descriptors¶
HARDENED_OFFSET, key_data_from_bip32_key and
descriptors.derived_origin are new public names (issue #620).
A test holds bip32 and descriptors to their boundary¶
tests/imports_test.py and tests/moving_imports_test.py check what the
files that move with the two units import. The wallet and slip132 cases
leave tests/descriptors (closes #620).
parse cuts a key, timelock or hash fragment at a brace as Core’s Expr does¶
Applies in wsh() and a tr() leaf, and changes the refusal of a { in
or after the fragment (closes #607).
SECURITY.md links what changed since the security review¶
(issue btclib-org/.github#1795).
An account-only SoftwareSigner is tested on a taproot psbt¶
That of bitcoindevkit/bdk_wallet#610. Tests only (closes #580).
REVIEWING.md’s own part opens its questions with .github’s sentence¶
(issue btclib-org/.github#1919).
uv.lock takes btclib 2026.10.28¶
With ecc 2026.10.28, and the strategies pin moves with it (closes #597).
The btclib pin is compared with the committed uv.lock¶
Not with the lock a deps-oldest or deps-latest run resolved (closes #602).
The keyword-only record holds only the sites btclib_wallet defines¶
(closes #600).
The presync peer’s silence is removed through monkeypatch¶
Which pytest 9.2.0’s undo requires (closes #592).
The integration workflows pin bitcoind 32.1¶
In place of 31.1 (closes #589).
The calls of btclib-org/.github name the commit of its tag v2026.10.29¶
With setup-uv 10.3.2 and setup-python 7.1.0, and coverage 7.17.0 and
uv-build 0.13.5 in uv.lock.
v2026.10.29¶
BIP322 finds a taproot annex inconclusive¶
On any taproot input of to_sign, once the required rules have passed
(closes #516).
A miniscript hash fragment takes Core’s IsHex¶
sha256(), hash256(), ripemd160() and hash160() refuse whitespace
and anything else that is not an even, non-zero count of hex characters
(closes #507).
CompactFilterFetcher checks blocks with btclib’s contextual rules¶
Block.assert_valid_contextual at the block’s height, in place of its own
copy of Core’s bad-witness-nonce-size (issue btclib-org/btclib#2705).
CompactFilterFetcher uses btclib’s headers sync¶
btclib.p2p.headers_sync and its parameters replace its own copy of
Core’s HeadersSyncState. The commitment is Core’s salted SipHash (issue
btclib-org/btclib#2706).
No socket is given more than the timeout of the call¶
_time_left and CompactFilterFetcher._remaining (closes #533).
bip38’s cipher comment cites ecc.alias.CipherF¶
Where it cited btclib.alias.CipherF, which btclib PR 2738 removes; ecc’s
definition gives the same reason (issue btclib-org/btclib#2710).
SECURITY.md and RELEASING.md verify a release by repository id¶
gh attestation verify takes --owner btclib-org, and the certificate’s
repository id, which a rename keeps, is compared with this repository’s,
where --repo compared the name (closes #517).
BIP322 is tested on rust-bitcoin/bip322’s consensus-invalid cases¶
Those of its pull request 89, at 27f33fa69b. Tests only (closes #570).
The bip32, bip322 and descriptor fuzz targets use what they accept¶
(closes #552).
fuzz_psbt is seeded with the valid BIP174 and BIP371 vector psbts¶
It holds what it accepts to a serialization fixed point and, where its input splits into maps of records, mutates records as bitcoin/bitcoin#36631’s PSBT mutator does on seven calls in eight (closes #541, closes #542, issue #543).
The calls of btclib-org/.github name the commit of its tag v2026.10.26¶
And its hook the tag. The hook pins also move markdownlint-cli2 to v0.24.0, check-jsonschema to 0.39.0 and ruff-pre-commit to v0.17.2.
The pyroma hook builds no dependency from source¶
It passes --no-build (issue btclib-org/.github#1763).
uv.lock takes bitcoin-core-rpc 2026.10.17¶
With hypothesis 6.170.5 and uv-build 0.13.3.
README.md says development is LLM-based¶
And asks that a contribution be read and understood by whoever submits it.
CONTRIBUTING.md and REVIEWING.md take the organization’s shared text
(issue btclib-org/.github#1846).
dependabot.yml says only uv_build has a ceiling in the dependency groups¶
(issue btclib-org/.github#1848).
v2026.10.26¶
BIP352 skips a taproot input whose control block has an invalid length¶
A script-path spend whose control block is not 33 bytes plus a multiple of 32, up to 128 of them, as bitcoin/bips#2339 has BIP352’s reference implementation do (closes #556).
The calls of btclib-org/.github name the commit of its tag v2026.10.25¶
claude-review.yml’s call included (issue btclib-org/.github#1870).
The check-sdist hook builds with uv_build<0.14¶
As [build-system] does (closes #511).
v2026.10.25¶
btclib-wallet depends on ecc, the renamed btclib-ecc¶
It requires btclib>=2026.10.24 and ecc>=2026.10.23, imports ecc, calls the
_var names of btclib and ecc, and raises Ecc* exceptions, with
ECC_NO_LIBSECP256K1 alone turning the bindings off (closes #477, closes #466).
btclib-wallet depends on bip39-slip39¶
It requires bip39-slip39>=2026.10.24 and raises Bip39Slip39ValueError and
Bip39Slip39TypeError, both Bip39Slip39Exception, for the BTClibMnemonics*
ones (issue btclib-org/bip39-slip39#106).
extract_tx refuses a fee above max_fee_rate¶
Core’s -maxfeerate by default, priced on the final virtual size
(closes #459).
BIP322 refuses a proof over the block sigop-cost limit¶
assert_as_valid adds the sigop cost of to_spend to that of to_sign
(closes #502).
BIP322 enforces more of its required rules¶
It checks them before the upgradeable transaction versions, and requires Non-Witness UTXO data for an additional legacy input (issue #493).
BIP322 refuses OP_CODESEPARATOR and a foreign signed message¶
In a P2WSH script and a tapscript leaf, and in a psbt whose signed message is not the one verified; the btcd corpus is pinned (issue #493).
An absent PSBT v2 output script is None¶
PsbtOut.script_pub_key is None where PSBT_OUT_SCRIPT is absent and
b"" where it is present and empty, which a v2 psbt keeps through
serialization. Its JSON form is null for absence.
CompactFilterFetcher bounds a header sync before its work reaches the minimum¶
It downloads such a chain twice, as Core’s headers presync does, and checks
a header as ContextualCheckBlockHeader does, less its version
(closes #354).
strip_checksum quotes no checksum¶
A wrong checksum is “invalid descriptor checksum” (closes #478).
A taproot input without PSBT_IN_SIGHASH_TYPE accepts SIGHASH_ALL¶
Beside SIGHASH_DEFAULT (closes #461).
A multi_a tapscript leaf is finalized¶
With the quorum Core’s finalizepsbt picks (closes #462).
parse reads multisig keys before the structure around them¶
And names Core’s fault for a refused older() or after() number, for a
multi() in tapscript and for a multi_a() in P2WSH (closes #437,
closes #438, closes #475, closes #476).
EsploraFetcher.get_balance reads an address’s balance¶
Balance and the BalanceReader protocol are published by fetch
(issue #458).
ElectrumFetcher.get_block_headers reads a chunk of headers¶
Each is checked as get_block_header checks one, and chained to the one
before it. Tested against florestad (closes #330).
parse is compared with Core and rust-miniscript¶
Over the descriptor corpus (closes #486).
rust-psbt’s vectors and btcd’s MuSig2 PSBTs are replayed¶
(closes #485, closes #498).
Generated PSBTs are compared with Core and bitcoin-s¶
In the weekly psbt-interop job (closes #333).
A test fails where the layering is broken¶
Where bip32 or psbt import upward, or btclib imports btclib_wallet
(closes #484).
The wide and nested thresh bound counts analyses, not seconds¶
(closes #499).
Known test failures stay out of the os-* sweeps¶
(closes #526, closes #546).
Dependabot’s Python reads of .github/integration-hwi are pinned¶
(closes #490).
The publish jobs use pypa/gh-action-pypi-publish v1.14.3¶
[tool.uv] required-version is >=0.13.4, past the emSign root incident
(issue btclib-org/.github#1844).
uv.lock takes fixes for its advisories¶
virtualenv 21.14.12 (btclib-org/.github#1824) and 21.14.13 (btclib-org/.github#1844), setuptools and filelock (btclib-org/.github#1849), GitPython 3.2.1 (btclib-org/.github#1835).
The integration-hwi speculos lock takes pillow 13.0.0¶
Past GHSA-654x-cwwv-5j9c and GHSA-5x8w-8jj5-q52h (closes #553).
AGENTS.md holds the instructions for agents¶
(issue btclib-org/.github#1808). dependabot.yml names the uv.lock
exception (issue btclib-org/.github#1818).
v2026.10.18¶
CompactFilterFetcher finds a wallet’s blocks over p2p¶
It reads the BIP157/158 filters one peer serves, downloads only the blocks whose filter matches, and gives the peer no script (closes #329).
A compact-filter block whose coinbase lacks the witness nonce is refused¶
At and above the segwit height, as Bitcoin Core does (closes #385).
parse and multipath_descriptors refuse a key with leading or trailing whitespace¶
With Core’s message less the key, “Key is invalid due to whitespace” (closes #443).
Leading zeros are accepted in descriptor and miniscript numbers¶
In older(), after(), thresh() and the thresholds of the multi
family, printed back without them, as Core does (closes #355).
multipath_descriptors hands every expansion to parse¶
It takes an optional network, reads each step on its first path so that
every key is read, and refuses what parse refuses, in the order Core reads
it (closes #393, closes #370).
multipath_descriptors prefixes a musig participant’s faults¶
The fingerprint and the origin ones, as parse does (closes #448,
closes #450).
An internal musig key is read before the later keys¶
In tr() and rawtr() (closes #451).
is_available accepts a path to hwi that Windows runs¶
On Windows, it also accepts a path with a directory part where that path
plus .exe is a file, if its last part has no period (closes #426).
The BIP445 sign/verify vectors are siv2r/bips 363f693’s¶
BIP445 0.12.1 (closes #369).
Script-path and timelocked spends are exercised on Core¶
Tests only (closes #460).
The work-count docstrings read once¶
Tests’ and _ScannedStr’s (closes #409).
SECURITY.md says a defect easy to rediscover is fixed as soon as possible¶
As btclib-org/.github sets it (issue btclib-org/.github#1771).
verified-signatures.yml checks the signatures on main¶
On each push (issue btclib-org/.github#1738).
The review call sets cache-mode: none¶
.github/actionlint.yaml is deleted (closes #402).
uv.lock takes pydantic 2.14.0¶
So the pyroma hook builds nothing from source on Python 3.15 (issue btclib-org/btclib#2685).
v2026.10.16¶
The btclib and btclib-ecc floors are 2026.10.16¶
btclib-wallet calls btclib-ecc’s _var names, and btclib through 2026.10.9
imports the names they replace (issue btclib-org/.github#1742).
psbt/frost calls BIP445 0.12.0’s interface¶
btclib-ecc’s frost follows it from 2026.10.10 (closes #338).
silent_payments.output_keys returns the keys in address order¶
It grouped them by scan key, so key i could be another address’s
(closes #415).
A DLC’s transactions are built and signed¶
The funding, execution and refund transactions of dlcspecs, with adaptor signatures, for the agreed terms only (closes #331).
DLC oracle announcements and attestations are read and verified¶
btclib_wallet.dlc.oracle reads, writes, signs and verifies them
(closes #332).
block_header_from_raw takes the network¶
Its proof-of-work limit is the network’s, so regtest and signet headers are read. electrs tests the Electrum and Esplora fetchers live (closes #328).
ElectrumFetcher.assert_network compares the genesis first¶
A server of another chain is a BTClibValueError, before any proof of work
(closes #341).
A global xpub key is a 78-byte xpub on the curve¶
As in Bitcoin Core (closes #342).
An input or output BIP32 derivation key is a point¶
Of 33 or 65 bytes, as in Bitcoin Core (closes #356, closes #377).
combine keeps the first psbt’s value on a key conflict¶
As Bitcoin Core’s Merge does (closes #325).
The psbt, MuSig2 and BIP85 refusals print no refused bytes¶
deserialize_map takes the map’s name for its refusal (closes #351).
An xpub whose key is not a point is refused by its prefix¶
No key byte is printed (closes #388).
Core’s multipath messages for a glued key and odd origin steps¶
multipath_descriptors and parse give them (closes #299, closes #301).
A bad origin fingerprint and a second multipath step come first¶
As in Bitcoin Core’s ParsePubkey (closes #353, closes #365).
The fault Bitcoin Core reads first in a key expression is reported¶
In multipath_descriptors and parse (closes #367, closes #397, issue #370).
KeyExpression.sec refuses an out-of-range index¶
As Descriptor does (closes #383).
Descriptor indexes that are not integers are refused¶
With BTClibTypeError (closes #404).
index_of and position_of refuse a bad last_index¶
One that is not an integer (closes #407), or is negative (closes #413).
DescriptorWallet refuses two branches holding the same descriptor¶
position_of could not tell them apart (closes #411).
DescriptorWallet refuses two branches sharing a script¶
However their descriptors are spelled (closes #414).
A bare HWI name is resolved against the PATH alone¶
Its absolute directories only, and on Windows as <name>.exe (closes #416).
An HWI name with a directory part is checked as the file it runs¶
Not through shutil.which (closes #422).
The hwi.py docstring says which whitespace is ignored¶
The ASCII whitespace btclib strips (closes #274).
Branch and bound prunes on waste and skips clones¶
As Core’s SelectCoinsBnB does, a tie going to the lower waste (closes #357).
The change output pays for the output count’s growth¶
When it is the 253rd output, as build_psbt prices it (closes #384).
Branch and bound is checked against a brute-force search¶
A property test over small pools (closes #327).
The fee estimate refusals carry their source once¶
For estimatesmartfee (closes #374), and an empty /fee-estimates reply has
its own message (closes #358).
The qa-assets corpora of Core and rust-bitcoin are replayed¶
Through the psbt and descriptor parsers (closes #340).
bitcoin-s’s descriptor cases and Trezor recording are run¶
The cases neither this suite nor Core has (closes #334), and the address recording, vendored (closes #321).
Two descriptor rules where rust-miniscript differs are pinned¶
Tests only (closes #392).
The descriptor and psbt fuzzers are seeded¶
From the BIPs and rust-bitcoin (closes #326, closes #335).
The descriptor fuzz harness checks round trips¶
Of the printed form and of each miniscript’s script (closes #345).
ClusterFuzzLite installs the build backend from the hashed lock export¶
Pinned and hashed, rather than resolved from the index (closes #319).
The multipath checks count their work instead of timing it¶
So load cannot fail them (closes #391).
electrs and bitcoind start again on fresh ports¶
When the port picked is taken (closes #410, closes #419).
The actionlint fork, api-break and the secret-free helper are ported¶
As btclib-org/.github sets them (issue btclib-org/.github#1698, issue btclib-org/.github#1674, issue btclib-org/.github#1697, issue btclib-org/.github#1723).
osv-scanner.toml covers the HWI environment¶
PYSEC-2026-1325 does not apply to it (closes #318).
RELEASING.md runs the dependents before the tag¶
And reads the tag back from the API (issue btclib-org/.github#1647, issue btclib-org/.github#1660).
REPOSITORY.md on the tag-integrity ruleset and strict off¶
What the ruleset does not check (issue btclib-org/.github#1635), and strict
off on main (issue btclib-org/.github#1712).
The maintainer’s bypass is everyday practice¶
A merge through it waits for no required check (issue btclib-org/.github#1695).
CONTRIBUTING.md on Dependabot uv pull requests and the secrets baseline¶
An owner commits the uv export (closes #336), and a .secrets.baseline
conflict is resolved by rescanning.
Prose corrections¶
The v2026.10.8 notes name GHSA-62jf-v327-86h5. psbt/musig2.py cites no
closed issue (closes #350). Review wording findings are applied (closes #389).
Bumps¶
The pre-commit hooks, the actions, the ClusterFuzzLite base image, bitcoin-core-rpc and btclib-mnemonics.
v2026.10.9¶
Descriptor.expand fills the scripts and the provider from one derivation¶
Descriptor.expand(index, prv_keys) returns what script_pub_keys and
provider return, as Bitcoin Core’s Descriptor::Expand does, deriving each
key once. provider alone derives each key once now too (closes #303).
Refusals no longer quote a key or a password¶
Errors no longer quote a refused import’s descriptor or Core’s message, an
Esplora URL holding @, a key that is not a point, a mistyped extended key’s
checksum or text, or a BIP38 password (GHSA-8qrf-j8jh-cx23, #311).
The btclib floor is 2026.10.8¶
btclib through 2026.10.7 quotes a refused network name whole (GHSA-c6h8-5hv5-3gv7, closes #306).
The chain-mismatch tests match both wordings of bitcoin-core-rpc’s refusal¶
They pass with the quoted wording, up to bitcoin-core-rpc 2026.10.4, and the unquoted one of 2026.10.8 (closes #307).
CONTRIBUTING.md names what uv sync builds from source on Python 3.15¶
pydantic-core is built from source there, which needs a recent Rust
toolchain (closes #278).
CONTRIBUTING.md and REVIEWING.md on resolving a review thread¶
The author resolves a review thread once answered (issue btclib-org/.github#1620, issue btclib-org/.github#1634).
v2026.10.8¶
A refused step in a key origin is not quoted¶
A refused multipath step in a key origin was quoted, with any private key
(WIF) in it, in the message of parse and multipath_descriptors. The message
leaves the step out, and so does the one below (#285).
multipath_descriptors and parse refuse a step that is not a whole path element¶
<0;1>h, <0;1>', 0<0;1>, <0;1>> and <<0;1> are refused as “Key path
value is not a valid uint32”, as Bitcoin Core does, except the last two in a
key origin, which Core refuses as a multipath step (closes #280).
The SIGHASH_DEFAULT refusal is named in the docstring of _assert_sendable_sig_hash_types¶
The docstring says the first check also refuses an explicit SIGHASH_DEFAULT
on an input not known to be taproot (closes #283).
CLAUDE.md points to CONTRIBUTING.md for the changelog rule¶
It names CONTRIBUTING.md’s Pull requests instead of restating that a pull
request adds no entry (issue btclib-org/.github#1630).
v2026.10.7¶
infer_descriptor answers what Bitcoin Core’s InferDescriptor does¶
descriptors.infer_descriptor returns the descriptor Core infers for a
script, from what a Provider knows; Descriptor.provider builds one, as
Core’s Expand does (closes #256).
btclib 2026.10.5 is the floor¶
It fixes GHSA-9r97-9x22-2pp4 and GHSA-rw95-w37r-537w, two slow paths in
transaction verification, which bip322.verify runs on a signature it was
given.
Bip21 refuses typed parameters passed in others¶
Bip21.assert_valid refuses others carrying amount, label, message or
lightning, instead of writing a URI its own parse refuses (closes #217).
The issue forms set a type, and the history files lose merge=union¶
Each form sets an issue type, no kind label (issue btclib-org/.github#1584); the history files have no merge driver (issue btclib-org/.github#1582); a release reviews the bestpractices.dev answers (issue btclib-org/.github#1589).
The post-release install refreshes the index¶
RELEASING.md installs the release with --refresh-package btclib-wallet,
because uv can answer from a cached index and miss a version just published
(issue btclib-org/.github#1595).
--admin waits for no required check¶
CONTRIBUTING.md’s emergency paragraph says --admin skips the
required checks too, and REVIEWING.md’s “hold the merge” excepts it
(issue btclib-org/.github#1597).
request_signatures sends another sig_hash type only if allowed¶
A psbt asking for a type other than SIGHASH_ALL or SIGHASH_DEFAULT is refused
before the signer is called, unless the new keyword-only
allowed_sig_hash_types names it (closes #233).
partial_sign of musig2 and frost refuses an input without its non_witness_utxo¶
Unless every input is taproot and none asks for ANYONECANPAY, as sign has
it. require_non_witness_utxo=False accepts it (closes #234).
A descriptor’s psbt derivations give every key an origin¶
The Updater gives a key without an origin one, as Bitcoin Core does. An
explicit origin wins, then a plain key’s, then the first spelling’s. A
musig() participant’s taproot entry lists its groups’ leaves (closes #213).
HwiSigner.sign_psbt sends another sig_hash type only if allowed¶
A psbt asking for a type other than SIGHASH_ALL or SIGHASH_DEFAULT is refused
before hwi runs, unless the new keyword-only allowed_sig_hash_types names
it (closes #243).
ScriptWallet gives a key without an origin the one Descriptor does¶
ScriptWallet’s Updater writes a key of a group given no origin under its own
fingerprint, as wsh() does. An explicit origin beats it, and of two explicit
ones for one key the first wins, where the last did (closes #236).
The release notes name a change to the release’s assets¶
RELEASE_NOTES.md gives the bundle’s name, v<version>.intoto.jsonl.
RELEASING.md asks that a change to a release’s assets or their verification
be named there (issue btclib-org/.github#1596).
check-changelog refuses an entry added to an older release¶
A ### heading under a release older than the newest, absent at the merge
base with origin/main, is refused (issue btclib-org/.github#1614).
The fuzzers bundle package data and accept btclib_ecc’s refusals¶
.clusterfuzzlite/build.sh bundles the package data of every pinned
dependency, which btclib_ecc’s curves need, and the fuzz harnesses
accept BTClibEccException as a refusal (closes #248).
fuzz fails on a crash it cannot reproduce¶
The run step passes language: python, whose reproduce timeout covers the
frozen binary’s startup, and report-unreproducible-crashes: true, so a crash
that still does not reproduce fails the run (closes #249).
SIGHASH_SINGLE with no output at its input’s index is never signed or sent¶
It commits to no output, and Bitcoin Core’s signer does not make it either
(bitcoin/bitcoin#35984). sign, request_signatures and HwiSigner.sign_psbt
refuse it, whatever is allowed (closes #258) (closes #263).
Corrupted psbts are tested against both readers¶
tests/psbt/psbt_mutation_test.py runs embit’s psbt bit-flip and truncation
cases against Psbt.parse and PsbtView: both refuse each corrupted psbt, or
both read it as given (closes #257).
parse accepts a pkh() leaf under tr()¶
tr(KEY,pkh(KEY)) parses to the miniscript leaf c:pk_h(KEY), as BIP386 lists
it and Bitcoin Core reads it. BIP386’s vectors and Core’s parse-error cases
are tested (closes #259) (closes #253) (closes #266).
BIP38 names hashlib.scrypt where the interpreter lacks it¶
Without hashlib.scrypt, encrypt, decrypt, intermediate_code and
new_key_pair raise a BTClibRuntimeError naming it, not an AttributeError
(closes #255).
Silent payment addresses use sprt on regtest¶
A regtest address has the hrp sprt, as in Bitcoin Core, and keys_from_address
reads it with the network type "test". Earlier releases give regtest tsp,
which Core’s regtest refuses. Testnet, testnet4 and signet keep tsp (closes #254).
Bolt11Invoice refuses malformed or repeated fields¶
from_invoice refuses a wrong-length or repeated p, s, h or n, a second
d, an x, c or 9 starting with a zero word, and an amount with a leading
zero. sign writes a zero x or c as an empty field (closes #260).
An explicit sig_hash_type of 0 is serialized and kept by combine¶
PsbtIn.serialize writes it, so sign refuses an ECDSA input asking for
SIGHASH_DEFAULT after a round trip too. combine keeps the first
sig_hash_type present, 0 included (closes #269).
HwiSigner and request_signatures refuse SIGHASH_DEFAULT unless taproot¶
An input asking for SIGHASH_DEFAULT that is not known to be taproot is refused before the signer is called, where a device signed SIGHASH_ALL (issue #269).
BIP174’s combiner vectors are tested in either input order¶
The walk-through’s combined psbt and the PSBT_IN_SIGHASH_TYPE vector are each
combined in either input order (closes #262).
multipath_descriptors refuses a multipath key Bitcoin Core refuses¶
Two <a;b> steps in one key path, a step in a key origin, and a musig()
multipath in both participants and path are refused with Core’s messages
(closes #265) (closes #279).
silent_payments.output_keys refuses an address of another network¶
It takes a network, "mainnet" by default, and refuses an address whose hrp
is not that network’s, as Bitcoin Core’s decoder does: tsp under regtest and
sprt under testnet are refused too (closes #270).
Wallet-policy templates refuse a function BIP388 does not allow there¶
wallet_policy_descriptor, wallet_policy_address and wallet_policy refuse
multi() or sortedmulti() at the top and pk() inside sh(); the first two
also refuse a top-level pk(), combo() or rawtr() (closes #267).
A test shows output_keys checks the network before it reads a key¶
test_output_keys_refuses_an_unknown_network_first passes only if an unknown
network is refused before an empty key list is (closes #287).
The changelog is written at release time¶
A release’s own pull request writes its sections of CHANGELOG.md and
RELEASE_NOTES.md, and no other pull request adds an entry. The issue forms
end with the sign-off notice (issue btclib-org/.github#1623).
A release is rehearsed once¶
RELEASING.md rehearses a release that breaks the public API from its pull
request’s branch, and again from main only where the branch changed after
that rehearsal (closes #291).
v2026.10.4¶
RELEASING.md’s griffe step searches src¶
griffe check takes -s . -s src, the pair release.yml’s public-api
job passes, where it answered ModuleNotFoundError for the previous
release (issue btclib-org/.github#1447).
pypi-install.yml installs the version the release published¶
The install names btclib-wallet==<version> from the tag release.yml passes,
where a bare name let a lagging index serve the release before it
(issue btclib-org/.github#1456).
CONTRIBUTING.md points a newcomer at good first issue¶
An issue carrying the label is small and self-contained: The issue tracker says so, and links the organization-wide search for the open ones (issue btclib-org/.github#1362).
The OpenSSF Baseline badge¶
README.md’s badge row ends with the OpenSSF Baseline badge, beside the
Best Practices badge, section 2 of the organization standard admitting it
on the same property (issue btclib-org/.github#1460).
pypi-install.yml retries the install of the version the release published¶
Each install cell runs pip install btclib-wallet==<version> through
btclib-org/.github’s install_published_release.py, which retries only while
the installer says the pin is not resolvable (issue btclib-org/.github#1458).
The release’s attestation bundle is attached as *.intoto.jsonl¶
RELEASING.md’s verification and recovery commands name the bundle
<tag>.intoto.jsonl, the name reusable-github-release.yml attaches it under
(issue btclib-org/.github#1468).
codeql.yml’s aggregate runs check_run_jobs.py¶
The aggregate’s step runs check_run_jobs.py, which reads the run’s jobs
listing again up to a deadline while a row of analyze is unfinished
(issue btclib-org/.github#1463).
generate_sbom.py carries a not-affected list into the bill of materials¶
generate_sbom.py reads .github/vex.toml, where the tree lists the
vulnerabilities its release is not affected by, into the document’s
vulnerabilities; no list, no key (issue btclib-org/.github#1469).
SECURITY.md promises a response time¶
SECURITY.md says a report is acknowledged within 7 days, and a fix or a
published advisory within 90 (issue btclib-org/.github#1460).
parse refuses impossible multisignature thresholds¶
descriptors.parse refuses a multi() or sortedmulti() threshold of zero
or more than its key count, instead of storing a descriptor that cannot
produce a script (closes #160).
parse refuses an impossible multi_a()¶
descriptors.parse refuses a multi_a() or sortedmulti_a() threshold of
zero or more than its key count (closes #172), and more than 999 keys
(closes #173).
release.yml audits the lock before it publishes¶
The audit job calls btclib-org/.github’s reusable-audit.yml, which runs
uv audit over what the wheel declares, and both publish jobs wait for its
success (issue btclib-org/.github#1466).
generate_sbom.py is btclib-org/.github’s¶
The dist job writes the bill of materials with btclib-org/.github’s
generate_sbom.py, served from main, and the tree keeps no copy of it
(issue btclib-org/.github#1478).
Dependency review is a required check¶
REPOSITORY.mdreadslint / Dependency reviewback with the other required checks (issue btclib-org/.github#1465).
[tool.uv] required-version is >=0.12.18¶
required-versionreads>=0.12.18, not>=0.12.19(issue btclib-org/.github#1482): the Dependabot service refused0.12.19withtool_version_not_supported.
SECURITY.md names the latest security review¶
SECURITY.md gives the date of the latest security review and links the issue
that records it (issue btclib-org/.github#1362).
A Signed-off-by: trailer on every commit of a pull request¶
Pull requests says every commit of a pull request carries a
Signed-off-by: trailer, and how to add it (issue
btclib-org/.github#1467).
The primary-checkout section uses one form for the checkout¶
The section writes the checkout as
"${checkout:?}"throughout, says what<scratchpad>is and names the pull (issue btclib-org/.github#1500).
The python inventory has a copy kept in the tree¶
docs/source/_inventories/python.inv is read when docs.python.org fails,
so an outage of that site no longer fails the -n -W docs build (issue
btclib-org/.github#1508).
public-api is red for a break RELEASE_NOTES.md does not name¶
release.yml and RELEASING.md say that a red public-api means
RELEASE_NOTES.md misses a name (issue btclib-org/.github#1517).
The release’s attestation is signed by reusable-build.yml, at SLSA Build L3¶
release.yml calls reusable-build.yml, which signs the files before the
publish jobs wait for approval (issue btclib-org/.github#1506).
CONTRIBUTING.md says the maintainer self-merges while the bot review is off¶
The review says no ack of record exists while claude-review.yml is off,
and that a local review of a named sha, by a reviewer other than the author,
stands in (issue btclib-org/.github#1527).
dependabot.yml does not say that every workflow passes --locked¶
.github/dependabot.yml says the workflows install from uv.lock with
--locked, bar the jobs that re-lock, install a published or built package
or install a hash-pinned export (issue btclib-org/.github#1538).
REPOSITORY.md reads back the web sign-off setting¶
REPOSITORY.md reads web_commit_signoff_required back, the organization
setting section 11 of the standard states (issue btclib-org/.github#1540).
descriptors.parse refuses a key of another network¶
parse raises BTClibValueError for a WIF or an extended key of another
network, as Core does; KeyExpression.wif_prefix holds the WIF’s version
byte for it (closes #180).
A musig() participant that is also a plain taproot key keeps its leaf hashes¶
TrDescriptor’s taproot derivations list every leaf of such a key whose plain
spelling has an origin, in either order; a participant written after the plain
key replaced its entry with an empty list (closes #185).
SilentPaymentOutput’s repr leaves out prv_key_tweak¶
The tweak is a secret, so a logged output does not carry it (closes #161).
mxprv_from_mnemonic takes only None or "" as the empty passphrase¶
In bip39 and electrum, b"", 0, False and [] are refused with
btclib_mnemonics’ BTClibMnemonicsTypeError, as slip39 refuses them,
where they derived the wallet without a passphrase (closes #162).
A malleable miniscript keeps neither s, f nor e¶
Miniscript.properties drops “s”, “f” and “e” from an expression without “m”,
as BIP379 asks, and is_signature_required is false there (closes #190).
The duplicate-key check compares derived public keys¶
Miniscript.has_duplicate_keys compares the public keys the KEY expressions
derive at index 0, as Bitcoin Core does. A key with a hardened step is
compared by its extended key and its path (issue #193).
The Sign-off check is required¶
CONTRIBUTING.md’s shared half says a pull request whose commits lack the
Signed-off-by: trailer cannot merge, and REPOSITORY.md lists
lint / Sign-off among the required checks (issue btclib-org/.github#1550).
The suite imports btclib_ecc’s exception classes from btclib_ecc¶
tests/exception_family_test.py imports them from btclib_ecc.exceptions,
since btclib.exceptions binds them only up to btclib 2026.9.29
(closes #166).
integration-hwi.yml installs HWI and Speculos from hashed locks¶
.github/integration-hwi/ holds hashed locks of both, installed with
--require-hashes, and the Ledger app is fetched by the commit that tag
2.5.0 names (closes #167).
The assurance case names every direct call into libsecp256k1¶
ASSURANCE_CASE.md also says verify_network runs before a fetcher’s first
answer, EsploraFetcher.text excepted, and names btclib-ecc’s dispatch
(closes #163).
The fuzz inventory covers the text parsers the threat model names¶
tests/fuzz_test.py drives the BOLT11, silent-payment address, BIP38,
minikey, derivation-path and transaction-or-PSBT parsers (closes #164).
The assurance case states the scope of mutation testing¶
ASSURANCE_CASE.md says a module no .github/mutation/ profile lists
rests on coverage alone (closes #165).
sign updates PSBT_GLOBAL_TX_MODIFIABLE of a version 2 psbt¶
Each signature added clears Inputs Modifiable unless it is ANYONECANPAY, clears Outputs Modifiable unless it is NONE, and sets Has SIGHASH_SINGLE where it is SINGLE, as BIP370 asks of the Signer (closes #194).
join clears the signatures of a version 0 psbt’s inputs¶
partial_sigs, the final scriptSig and witness, and the taproot and MuSig2
signature fields are emptied, so the joined psbt can be signed again
(closes #195).
extract_tx verifies the scripts and the amounts by default¶
extract_tx runs every input’s scripts under the consensus rules and checks
that the outputs do not exceed the inputs. verify_scripts=False extracts
without them. bip322 passes it, running the scripts itself (closes #191).
The parsers refuse a long number as a BTClibValueError¶
A derivation index, a multisignature or thresh() threshold, an older() or
after() number and a wallet-policy @N of over ten significant digits are
refused, where int() raised ValueError past 4300 digits (closes #158).
A miniscript parse stops at its script size and at 1000 thresh() arguments¶
A parse refuses once the fragments it has built pass the context’s script
size, before it analyses the thresh() they are arguments of. A thresh()
of more than 1000 arguments is refused, a limit of this library (closes #159).
Bolt11Invoice.from_invoice refuses a long amount as a BTClibValueError¶
An amount of more than 20 digits, leading zeros apart, is refused, where
int() raised the built-in ValueError past 4300 digits (closes #205).
CONTRIBUTING.md says the ack of record is a bot’s¶
The review says the maintainer lands their own pull requests through the
bypass, and that the OpenSSF criterion two_person_review is unmet because
the ack of record is a bot’s (issue btclib-org/.github#452).
The bypass is for emergencies, in CONTRIBUTING.md and this tree’s prose¶
CONTRIBUTING.md and REVIEWING.md say every pull request, the maintainer’s
included, lands with an approving review from somebody other than its author;
the bypass is for emergencies (issue btclib-org/.github#1362).
Earlier entries on how a pull request lands¶
Entries above that have the maintainer landing without another person’s approval describe the rule before issue btclib-org/.github#1362 (issue btclib-org/.github#1569).
A PSBT’s global unsigned transaction is read as Core reads it¶
It is read as TX_NO_WITNESS, so a PSBT with no input is read. The floors are
btclib 2026.10.3, the first release with Tx.parse_without_witness, and
btclib-ecc 2026.10.2, the one that release declares (closes #196).
A musig() participant with an origin keeps the leaf hashes of its plain key¶
TrDescriptor’s taproot derivations give a key that is a plain leaf key and a
musig() participant the leaf hashes of its plain leaves, whichever spelling
has the origin (closes #198).
join refuses to change the lock time or tx version of a signed version 2 psbt¶
join refuses to change the lock time or the tx version of a version 2 psbt
with a signed input, whose signatures commit to both. It kept signatures that
no longer verified (closes #211).
Miniscript’s duplicate-key and sanity checks are methods¶
has_duplicate_keys, is_sane, is_sane_subexpression and insane_sub
were properties. They are methods taking prv_keys (issue #199).
The duplicate-key check derives a hardened step¶
A key with a hardened step is compared by the public key it derives where the descriptor holds its private key, not by its extended key and path. A key not derived is compared as written (closes #199) (closes #193).
The release publishes the files the build job built¶
The publish jobs, github-release and test.yml’s dist job on a release
stop where the dist they download differs from the digests
reusable-build.yml outputs (issue #168).
The verification command pins the tag for every signer¶
SECURITY.md and RELEASING.md said a release signed by reusable-attest.yml
took no --source-ref; it takes the tag (issue #168).
The weekly vendored-vectors check compares bytes¶
check_vendored_vectors.py hashes each vendored file against the blob
tests/_data/README.md records, and that blob against upstream’s at the
pinned commit, failing the run on a mismatch (issue #168).
A non-mapping prv_keys is a BTClibTypeError¶
The methods taking prv_keys and descriptors.normalized check it as parse
does, where a string left as an AttributeError; a test finds every such
callable (closes #215).
btclib 2026.10.4 and btclib-secp256k1 0.8.0.10 are the floors¶
They fix GHSA-9fr5-46w5-5f9r, which stalled bip322.verify on a crafted
tapscript witness, and GHSA-8h6f-34jj-7p6c, in silentpayments.scan_outputs.
sign and partial_sign refuse a sig_hash type the caller did not allow¶
So does SoftwareSigner.sign_psbt: any type but ALL or DEFAULT needs
allowed_sig_hash_types, and a legacy SINGLE input past the last output is
never signed (GHSA-qq38-77mp-j6wr).
ecdsa_sig_hash refuses a legacy SIGHASH_SINGLE past the last output¶
So does PsbtView.ecdsa_sig_hash: the hash is the constant 1, which no
allow-list makes signable (GHSA-qq38-77mp-j6wr).
An input with no sig_hash type takes ECDSA signatures of SIGHASH_ALL alone¶
finalize, assert_signed and assert_signatures_only, which
request_signatures runs, refuse another type there (GHSA-qq38-77mp-j6wr).
sign refuses an input without its non_witness_utxo¶
Psbt.assert_signable, sign and SoftwareSigner.sign_psbt refuse an input
without its non_witness_utxo unless every input is taproot and none asks for
ANYONECANPAY; require_non_witness_utxo=False accepts it (GHSA-v4gq-j2v2-c4jp).
v2026.9.30¶
README.md carries the OpenSSF Best Practices badge¶
It follows the Scorecard badge, where section 2 of the organization
standard places it for a tree section 10’s scorecard entry names; the
project is bestpractices.dev’s 14813 (issue btclib-org/.github#350).
A release’s version is the date it is cut¶
RELEASING.md dates a release YYYY.M.D of the day it is cut, not the
placeholder’s month with the day added (closes #5).
The suite the sdist ships is run from the unpacked sdist¶
test.yml’s dist job unpacks the sdist it built and runs the suite
there, gated at 100% coverage (closes #3).
sdist-rebuild.yml stops passing attest-signer¶
The called workflow verifies against reusable-attest.yml alone, so the
input decides nothing (issue btclib-org/.github#1315).
The type aliases only the wallet uses are defined in the wallet¶
BIP44ScriptType, BlockCipherF, EmbeddedScriptType, KeyOrder,
MnemonicLang and ValidSigHashType are published by this package’s own
modules, not imported from btclib.alias (issue btclib-org/btclib#2244).
input_validation_test.py checks this package’s own aliases too¶
An alias a public parameter takes, from btclib’s alias.py or this package,
is driven or exempted with a reason, and the walk drives Entropy; BinStr
and Mnemonic wait on their functions’ validation (closes #12, issue #15).
A mnemonic or a binary-string entropy of another type is refused¶
The functions taking one refuse any other type with a BTClibTypeError,
bytes included, and input_validation_test.py drives BinStr and
Mnemonic (closes #15).
codeql-passed and test-passed no longer skip while draft¶
A skipped required check reads as passing, so both aggregates now fail a first step on the draft flag instead of skipping on it (issue btclib-org/.github#1327).
CPython 3.15 is the interpreter pinned and a version claimed¶
.python-version names 3.15, its release candidate counting as a
release, and the classifiers and the sweeps gain it; requires-python
stays at 3.11 (issue btclib-org/.github#1324).
pre-commit.ci skips uv-lock¶
Its image lacks the interpreter .python-version names and has no
network to download it; the lint workflow still runs the hook (issue
btclib-org/.github#1348).
The rebuild of a release names the interpreter its tag pinned¶
RELEASING.md’s Rebuild a release from its tag reads it from the tag’s
.python-version rather than naming one (issue btclib-org/.github#1349).
The rebuild of a release runs in a worktree of its tag¶
RELEASING.md’s Rebuild a release from its tag builds in a worktree of
the tag, clean by construction, and drops the git archive export, which
has no .git for its git commands (issue btclib-org/.github#1352).
A drift line names both commits whole¶
check_vendored_vectors.py prints the pinned commit and upstream’s tip as
full shas, in its output and in the tracking issue, so two commits alike in
their first twelve characters print as two (issue btclib-org/.github#1343).
ARCHITECTURE.md and ASSURANCE_CASE.md join the root¶
The architecture moves there from CLAUDE.md, which points at it; the
assurance case cites the tree for every claim (issue
btclib-org/.github#1321).
The rebuild of a release builds under the release’s own uv¶
RELEASING.md’s Rebuild a release from its tag builds under the uv
the published wheel names and verifies the sdist first: a wheel that
disagrees stops the chain after it (issue btclib-org/btclib-node#1063).
The publishing environments’ required reviewer is any of the owners¶
RELEASING.md and REPOSITORY.md name fametrano, giacomocaironi and
pmazzocchi as the pypi and testpypi reviewers, not fametrano alone
(issue btclib-org/.github#1355).
CONTRIBUTING.md and README.md link GOVERNANCE.md and ROADMAP.md¶
Both point a contributor at the organization’s one copy of each, in
btclib-org/.github (issue btclib-org/.github#1359).
tests.no_bindings finds the dispatch from set_libsecp256k1_serving¶
It refuses the bindings held by the module the switch’s __module__ names,
ellipticcurves.curves.curve under a btclib re-exporting that switch, and
no_bindings_anywhere walks ellipticcurves too (closes #52).
bip32_test and key_wallet_test find what they patch from a public name¶
The mod_sqrt_var patch and the counted key derivation land in the module
CurveGroup’s and bytes_from_prv_key_int’s __module__ names, which
holds under a btclib re-exporting ellipticcurves (closes #55).
The suite refuses a dispatch switch the installed btclib ignores¶
py_arm_authority_test.py’s measurement sets BTCLIB_NO_LIBSECP256K1
and ELLIPTICCURVES_NO_LIBSECP256K1; a run with either set while
libsecp256k1 still serves exits 4 rather than measure it (closes #53).
A refusal of ellipticcurves’ own leaves as that package’s class¶
The contract’s tests accept its classes beside btclib’s, never a bare built-in;
a psbt or musig() key it refuses is a BTClibValueError, and bip322.verify
answers False to its runtime error (closes #54).
The curve package is btclib_ecc¶
Where the entries above spell it ellipticcurves, it is btclib_ecc, with
BTClibEcc* classes and BTCLIB_ECC_NO_LIBSECP256K1; a name btclib leaves
unbound while delegating to it fails the suite (issue btclib-org/btclib#2282).
REVIEWING.md lets a filed issue carry its fix¶
An issue filed from a review may say the fix where one is known: What is filed, and what is not dropped its “no fix”, the filing bar standing as it was (issue btclib-org/.github#1378).
The ClusterFuzzLite base image is pinned by digest¶
.clusterfuzzlite/Dockerfile’s FROM names a sha256 digest, and
.github/dependabot.yml carries a docker ecosystem entry on
/.clusterfuzzlite to move it forward (closes #32).
The ClusterFuzzLite build installs from uv.lock, not the index¶
.clusterfuzzlite/build.sh installs a hashed uv export --locked
requirements file with pip3 install --require-hashes before installing
the package itself --no-deps (closes #33).
SLIP-0039 generation refuses an argument of another type¶
slip39.mnemonics_from_master_secret refuses one with a BTClibTypeError,
groups included, and a bool where an integer belongs (closes #43).
Dice rolls are shuffled as a copy, and refused unless integers¶
bin_str_entropy_from_rolls leaves the caller’s rolls in order, reads a
tuple, and refuses with a BTClibTypeError what is no sequence of
integers (closes #47).
TlsLineTransport’s default context sets TLS 1.2 as its floor¶
The context it builds when given none has minimum_version set to
TLSv1_2: PyPy’s ssl.create_default_context() leaves it at
MINIMUM_SUPPORTED, where CPython’s already answers TLSv1_2 (closes #46).
EsploraFetcher and ElectrumFetcher check their constructor arguments¶
A base_url that is no http(s) url with a host, a timeout that is no
positive finite number, or a transport that is not callable is refused
at construction, as a BTClibTypeError or BTClibValueError (closes #44).
EsploraFetcher refuses a base_url carrying credentials¶
A url with a user or a password is a BTClibValueError that does not echo
it, since the HttpError of a status other than 200 quotes the url
(closes #65).
BitcoinCoreFetcher and BitcoinCoreRestFetcher check their client¶
A client of another class is a BTClibTypeError at construction, and so
is an estimate_mode that is no string; a string that is none of the modes
Core takes, in any ASCII case, is a BTClibValueError (closes #64).
A timeout past threading.TIMEOUT_MAX is a BTClibValueError¶
TlsLineTransport, EsploraFetcher and ElectrumFetcher share one check,
transport.valid_timeout, bounded where a socket still waits; past it, a
timeout left the transport’s call as a bare OverflowError (closes #70).
The change script is a required argument¶
tx_builder.build_psbt, coin_selection.select_coins, knapsack and
single_random_draw take change_script_pub_key with no default, a sweep
passing None; branch_and_bound keeps its default (closes #36).
build_psbt refuses a lock time that final sequences void¶
A non-zero lock_time where every input’s sequence is 0xffffffff is a
BTClibValueError, consensus ignoring the lock time of such a
transaction (closes #37).
build_psbt refuses an output below the dust threshold¶
An output being paid worth less than dust_threshold for its script, at
the dust_fee_rate the change is created against, is a
BTClibValueError (closes #42).
build_psbt refuses a fee above max_fee¶
The keyword defaults to DEFAULT_MAX_FEE, Bitcoin Core’s -maxtxfee
default of 0.10 BTC, and a larger fee is a BTClibValueError
(closes #48).
psbt.sign runs BIP375’s Signer checks on a silent payment¶
Before any key is asked, it refuses what psbt.silent_payments.assert_as_valid
refuses, a sighash other than SIGHASH_ALL among it, and a silent payment
output with no script yet (closes #34).
psbt.extract_tx runs BIP375’s Extractor check on a silent payment¶
check_validity also runs psbt.silent_payments.assert_as_valid, which
reads a finalized input’s key from its final scripts, and refuses a silent
payment output with no script (closes #35).
psbt.combine merges a silent payment output’s script¶
The script is taken from whichever psbt carries it, and two different ones
are refused with a BTClibValueError rather than one kept (closes #50).
build_psbt refuses a fee above what max_fee_rate asks¶
The keyword defaults to DEFAULT_MAX_FEE_RATE, Bitcoin Core’s
-maxfeerate default of 0.10 BTC/kvB, applied to the estimated vsize;
a larger fee is a BTClibValueError (closes #73).
The suite refuses a constant default entropy in BIP39 and SLIP-0039¶
bip39_test and slip39_test generate twice or more with no entropy given
and assert the mnemonics, the SLIP-0039 identifiers and the share values
differ, and that a default BIP39 entropy is 128 bits (closes #41).
HwiSigner keeps caller text off HWI’s options, and the psbt off argv¶
A message, a policy name or a registration starting with - reaches HWI
as a value (closes #45), a psbt goes to its standard input rather than to
a size-capped argv (closes #51), and a NUL in an argument is refused.
A psbt input’s witness utxo has to be the output its non-witness utxo names¶
Psbt.assert_valid refuses one differing in amount or script, and the
spent output is read from non_witness_utxo as Bitcoin Core reads it;
bip322 does both against an earlier input’s transaction (closes #40).
estimated_input_sizes checks the utxo against the outpoint¶
A non_witness_utxo not the transaction the TxIn names, or without the
output it names, is a BTClibValueError, as is a witness_utxo differing
from that output, no longer read in its place (closes #80).
request_signatures runs BIP375’s Signer checks before a signer is asked¶
A psbt paying a silent payment is refused as psbt.sign refuses it before
sign_psbt is called, so a signer asked through it sees none (closes #74).
Which inputs a silent payment sums is decided by the script they spend¶
A counted input without a key its script commits to is refused, not left out,
and set_output_scripts waits for every counted input’s share (closes #76).
HwiSigner.sign_psbt runs BIP375’s Signer checks before hwi runs¶
A psbt psbt.sign would refuse is refused on a direct call too, not only
through request_signatures, and a request that is no psbt is a
BTClibTypeError (closes #82).
psbt.combine validates the psbt it returns¶
Copies valid alone whose merge is not, such as one’s witness_utxo
contradicting another’s non_witness_utxo, are a BTClibValueError
(closes #81).
psbt.combine refuses no psbts and what is not a psbt¶
An empty sequence is a BTClibValueError, and a psbts that is no sequence
or holds an element that is no Psbt a BTClibTypeError (closes #88).
psbt.join refuses no psbts and what is not a psbt¶
An empty sequence is a BTClibValueError, and a psbts that is no sequence
or holds an element that is no Psbt a BTClibTypeError (closes #89).
CLAUDE.md names three failures a session met in this tree¶
The union seam in RELEASE_NOTES.md that no hook reports, the
.secrets.baseline conflict on generated_at that costs a review round,
and the timeout macOS does not ship.
integration-hwi.yml installs bitcoind with .github’s script¶
It runs install_bitcoind.py, as the reusable workflow that
integration-bitcoind.yml calls does, and the tree’s own install-bitcoind
action is dropped (issue btclib-org/.github#1373).
codeql.yml, REPOSITORY.md and dependabot.yml follow sections 10 and 11¶
The aggregate accepts analyze’s lagging rows (issue btclib-org/.github#1395),
signatures and SHA pinning are read back (issue btclib-org/.github#1409), and
pre-commit is an ecosystem left unused (issue btclib-org/.github#1391).
A malformed descriptor’s refusal no longer echoes its private keys¶
descriptors.parse and wallet_policy_descriptor name what is wrong without
quoting the descriptor text around it, a WIF or an xprv included (closes #49);
btclib’s bech32 decoder can still quote an addr() argument (issue #94).
KeyExpression refuses an extended private key as xkey¶
With a BTClibValueError that does not quote it, so no KeyExpression holds
an xprv for its str, or a wallet-policy refusal quoting it, to echo
(closes #95).
KeyExpression’s xkey is an xpub, a placeholder or nothing¶
Other text is refused without being quoted: an xprv damaged by a character decodes as no key, and where only its checksum changed it still spells the key (closes #97).
A mixed-case or non-ASCII address is not read as the one it lowers to¶
silent_payments.keys_from_address refuses both, and a wallet lowers only an
ASCII all-uppercase spelling: neither a mixed-case string nor U+212A KELVIN
SIGN in place of K is found as the address it lowers to (closes #99).
descriptors_test matches the refusal of a non-script without its quote¶
test_index_of_refuses_what_it_could_only_answer_none_for matches
neither a script nor an address alone, which a btclib carrying
btclib-org/btclib#2350 raises without the string it refused (closes #101).
The regtest job exempts the HWI skips by their message¶
integration-bitcoind.yml passes skip-reason-prefix: "set BTCLIB_HWI" for
exclude-classname: hwi: an HWI testcase skipping for another reason fails
the check that the node’s tests ran (issue btclib-org/.github#1419).
Address, key and base64 readers strip ASCII whitespace alone¶
str.strip() also takes U+00A0, U+3000 and the rest of what str.isspace
counts; string.whitespace is Bitcoin Core’s IsSpace. tx_or_psbt_from_any
refuses non-ASCII text with BTClibValueError, not ValueError (closes #102).
mnemonic.entropy reads a string entropy in ASCII digits alone¶
int read 0x1_0, 1_6, +16, non-ASCII digits and U+3000 padding as 16,
collect_rolls read +2 and those digits, and a binary string took -, 0b
or _. No refusal quotes the entropy, an index or a roll (closes #103).
descriptors.parse refuses a multisig where Bitcoin Core refuses it¶
A bare multi() of more than three keys, a redeem script inside sh() over
520 bytes and more than twenty keys anywhere are refused, as Bitcoin Core
refuses them; the sh() one gave an unspendable address (closes #109).
Psbt.b64decode refuses a character outside the base64 alphabet¶
Past the ASCII whitespace stripped from either end, a line break or a U+001C
is refused with BTClibValueError, as Bitcoin Core’s DecodeBase64PSBT
refuses it, where the decoder dropped it and read the rest (closes #108).
A derivation path’s step is ASCII digits, padded with ASCII whitespace¶
A step spelled 1_0, +1, -0 or in non-ASCII digits, or padded with U+3000,
is refused without being quoted, and so is a ninth character other than / in
BIP32KeyOrigin.from_description, where each was read as a path (closes #107).
codeql-passed reads a lagging row again and fails on a failed analyze¶
An unfinished analyze row is read again up to three times, 10 s apart, before
it is accepted (issue btclib-org/.github#1416), and an analyze result other
than success or skipped fails the check (issue btclib-org/.github#1424).
A / in a key expression is followed by a path step¶
descriptors.parse and miniscript.parse refuse [deadbeef/], xpub.../,
xpub...//* and musig(...)/, as Bitcoin Core refuses the empty step, where
each was read as though that / were absent (closes #116).
The base64 readers read the canonical encoding alone¶
YR==, which decoded as YQ==, and an = after a whole group, which CPython
and PyPy 3.11 decoded, are refused as Bitcoin Core’s DecodeBase64 refuses
them; bip322.Sig.b64decode does not quote a non-ASCII character (closes #114).
Bip21.parse hands the amount to btclib as a Decimal¶
amount=1., amount=.5 and amount=01 parse under a btclib carrying
btclib-org/btclib#2378, which refuses them as text. Too many decimals are
refused quoting Decimal’s 1E-9, not the URI’s 0.000000001 (closes #122).
bip85 names the path reader as what refuses too many sides or rolls¶
The comment above _MIN_SIDES names indexes_from_der_path, not
str_from_index_int, which neither value passes through, and the suite pins
the refusal of 2**31 sides or rolls (closes #123).
The vendored psbt BIPs’ pins move to upstream’s tip¶
BIP375’s tip adds an invalid vector, a wrong-length
PSBT_OUT_SP_V0_LABEL field, which tests/psbt/bip375_test.py now
refuses beside the other field-shape rules (closes #128).
The pinned btclib-org/.github rev is allowlisted inline¶
detect-secrets reads the pinned sha as a secret, and the baseline recorded
only the previous one: a # pragma: allowlist secret on the rev: line
holds for any sha, and the stale entry goes.
The curve arithmetic and the schemes are imported from btclib_ecc directly¶
btclib.curves, btclib.ecc less bms, btclib.kdf and btclib.number_theory
become btclib_ecc’s own modules; btclib’s own floor moves to the release
binding its own objects, and btclib-ecc joins dependencies (closes #127).
A PSBT_IN_TAP_LEAF_SCRIPT record with an odd leaf version is skipped¶
BIP341 makes a leaf version’s low bit always 0; the Signer excludes such a record from what it proves and what it signs, matching Bitcoin Core, rather than raising or masking the bit away (closes #126).
[tool.uv]’s floor rises to the uv dependabot-core bundles¶
required-version reads >=0.12.19, the pin in dependabot-core’s
uv/Dockerfile: the old floor admitted a uv older than the one the updater
writes uv.lock with (issue btclib-org/.github#1438).
The mnemonic calls that leaked a builtin TypeError refuse as btclib¶
bip39.seed_from_mnemonic’s passphrase, WordLists.index and the other
calls the issue lists refuse a wrong type by the parameter’s name (closes #134).
A mnemonic lang of another type is refused as a type¶
WordLists.load_lang, which every function taking a lang reaches,
refuses one with a BTClibTypeError, and so does dispatch, which answered
"" for it (closes #136).
bip85.mnemonic_from_root_key refuses a lang of another type as a type¶
A hashable one was refused as an unnumbered language, and an unhashable one
left as a builtin TypeError from the table lookup (closes #138).
A mnemonic refusal quotes no word, checksum, padding or hash prefix¶
An unknown word is named by its position, a word-list index out of range is refused by its position, and the rest by what they are (closes #135, closes #137).
RELEASING.md’s bundle verification names the signer workflow¶
The --bundle form of Verify the provenance of an asset passes
--signer-workflow "$signer", without which gh attestation verify refuses
a good release (issue btclib-org/.github#1446).
An addr() argument is not echoed by btclib’s bech32 refusals¶
From 2026.9.29, the floor dependencies holds, btclib’s bech32 decoder quotes
nothing it refuses, so the addr() exception the #49 entry makes no longer
holds: descriptors.parse does not repeat a key written there (closes #94).
The bindings floor is the one btclib’s secp256k1 extra declares¶
btclib-secp256k1, named in the secp256k1 extra and the bindings group,
takes btclib’s >=0.8.0.9 (closes #105).
The mnemonic schemes are btclib-mnemonics’, a new dependency¶
btclib_wallet.mnemonic keeps what builds a key from a BIP39, SLIP39 or
Electrum seed; the schemes up to the seed are btclib_mnemonics’, and
no module here re-exports them (closes #30).
A timeout is bounded by what a socket waits for on every platform¶
transport.MAX_TIMEOUT, the whole seconds within INT_MAX milliseconds,
bounds valid_timeout, where threading.TIMEOUT_MAX let through one a
Windows socket or a PyPy one refuses (closes #146).
v2026.9.24¶
The wallet layer is a package of its own, btclib-wallet¶
The modules above btclib’s primitives leave btclib for this package,
each at the same path under btclib_wallet (issue btclib-org/btclib#2129).
btclib-wallet imports only btclib’s public names¶
The floor is btclib>=2026.9.24, the first btclib release publishing
every name imported here, and the secp256k1 extra names the bindings
this package imports directly (issue btclib-org/btclib#2242).
Python 3.11 and up¶
requires-python is >=3.11, and the classifiers start at 3.11.
The interpreters this package claims are the ones it runs on¶
tests/interpreters_test.py refuses a floor, a classifier list and a
platform sweep that disagree on which Pythons this package supports.
The PSBT, descriptor, BIP32 and BIP322 parsers are fuzzed weekly¶
fuzz.yml runs a ClusterFuzzLite target for each parser that reads a
stranger’s octets or text, seeded from the vectors the suite already holds.
The sdist leaves out the tests that read .github or fuzz/¶
Those tests fail when run from an unpacked sdist, which carries neither
directory, and tests/source_exclude_test.py refuses one not in
source-exclude (closes #1).
REPOSITORY.md records what each read-back answers¶
Each read-back carries what it answered on 2026-09-24, and the one for the
repository’s switches asks for .default_branch by its field
(issue btclib-org/btclib#2129).