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).

Shared files follow btclib-org/.github

Sourcery, fuzz’s cadence, vendored vectors, the docs block, RootFileLinks (issue btclib-org/.github#2162, btclib-org/.github#2190, btclib-org/.github#1927, btclib-org/.github#2152, btclib-org/.github#2015, btclib-org/.github#2166).

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).

Shared files take btclib-org/.github’s copies

RELEASING.md, CONTRIBUTING.md and the tests’ secret_free helper (issue btclib-org/.github#2107, issue btclib-org/.github#2145, issue btclib-org/.github#1950).

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).

The silent-payment rule on a shared scan key is in the docstrings

(closes #705).

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).

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).

CONTRIBUTING.md takes btclib-org/.github’s shared part

(issue btclib-org/.github#2088).

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).

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).

RELEASING.md, CONTRIBUTING.md and REVIEWING.md take the shared part

RELEASING.md gains the pinned griffe step and the api_gaps.py run (issue btclib-org/.github#1855).

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).

CONTRIBUTING.md’s shared part is in line with btclib-org/.github

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).

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).

REVIEWING.md’s shared part is btclib-org/.github 72e0222’s

(issue btclib-org/.github#1904).

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 refusal tests call the shared assert_secret_free

(closes #396).

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).

CONTRIBUTING.md takes the organization’s shared half

The tree’s docs drop the maintainer’s bypass (issue btclib-org/.github#1740).

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).

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).

CLAUDE.md carries the organization’s shared section on the primary checkout

Its section The primary checkout is the maintainer’s is the organization’s shared text, and ARCHITECTURE.md points the libsecp256k1 dispatch at btclib-ecc’s SECURITY.md (issue btclib-org/.github#1494).

Dependency review is a required check

  • REPOSITORY.md reads lint / Dependency review back with the other required checks (issue btclib-org/.github#1465).

[tool.uv] required-version is >=0.12.18

  • required-version reads >=0.12.18, not >=0.12.19 (issue btclib-org/.github#1482): the Dependabot service refused 0.12.19 with tool_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).

SLIP-0039’s list of shares is refused when it is not one

master_secret_from_mnemonics and mxprv_from_mnemonics refuse anything but a sequence of mnemonics, a lone mnemonic included, with a BTClibTypeError (closes #17).

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).

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).

A mistyped mnemonic or SLIP-0039 share is no longer echoed whole

bip39.py, electrum.py and slip39.py reported the sentence in the exception raised over an unrecognized language or a bad checksum; each now reports the word count instead (closes #38).

slip39.Share’s repr no longer prints value

value is the (encrypted) master secret, and the frozen dataclass’s default repr printed it; BIP32KeyData.__repr__ masks its key material for the same reason (closes #39).

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).

A SLIP-0039 share, and its passphrase, refuse a value of another type

slip39.Share, mnemonic_from_share and an entropy source’s draws are type-checked, and mxprv_from_mnemonics reads only None as the empty passphrase, refusing a falsy value of another type (closes #63) (closes #66).

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).