Release notes

Notable changes are documented here. CHANGELOG.md is the record behind them: this file says what a user has to act on, that one says what changed.

Versions are calendar versions, YYYY.M.D: the number says when a release was cut, and promises nothing about compatibility, so a breaking change is announced in this file — read it before upgrading, rather than a digit.

v2026.11.13

bip44 discovers a key’s used accounts, and several calls answer or refuse differently: a caller that relies on one of the answers below, or matches the text of a refusal changed below, breaks. Against v2026.11.12, the breaks griffe reports in the public API are in the last bullet. api_gaps.py reports none.

  • bip44.discover_account and bip44.discover_accounts find used accounts from public data (ISS 816), as BIP44’s account discovery does, reading the change branch too. discover_account scans one account’s descriptors; discover_accounts takes account xpubs and refuses an extended private key.

  • A ledger holds its file’s exclusive lock from create, load or restore until close (ISS 800), as Core holds a wallet’s. A second Ledger on one file, in one process or two, and a restore from the file are refused at once; another SQLite connection is refused once its busy timeout runs out. Act on it if you open one file twice, or copy it while a ledger holds it: copy it with Ledger.backup.

  • Ledger.sign refuses as locked, not as a damaged file, where another thread locks or closes the ledger while it decrypts a source’s keys (ISS 801). Act on it if you match the text of that refusal.

  • A descriptor whose miniscript tr() leaf names two keys of one x under two parities is refused (ISS 824), where it was accepted, as BIP379’s duplicate-key rule asks: a tapscript pushes only the x. A bare multi_a or sortedmulti_a leaf is still accepted, as Core accepts it.

  • ElectrumFetcher.get_history and EsploraFetcher.get_history keep only the transactions that pay to or spend from the scripts asked (ISS 703), where they returned every transaction the server listed. Where only a spent transaction the history does not return can settle whether a listed one touches the scripts, they ask get_tx for it, and refuse one the server does not serve. Act on it if you relied on the server’s whole list.

  • ElectrumFetcher.get_history refuses, with FetchError, a confirmation, or the block the history starts from, whose header does not chain to the tip the server reports (ISS 708, ISS 873), where it checked each header on its own. It asks for the headers from start_height up to the next header it has already chained: the tip, or one from an earlier call, one request per chunk.

  • Psbt.b64decode and tx_or_psbt_from_any refuse a base64 or hex text longer than a 16 MiB psbt’s before decoding it (ISS 823), where they decoded a text of any length.

  • psbt.finalize refuses a single-key input whose key the script does not hash, and an uncompressed p2wpkh key, wrapped or not, with BTClibValueError (ISS 799, ISS 831), as Core’s finalizepsbt leaves such an input incomplete. A p2sh-p2pkh input’s script_sig now carries its redeem script (ISS 830).

  • A p2sh-wrapped p2tr program is a witness program of no known rule (ISS 804, ISS 818): sign gives it no taproot signature, and asks for its non_witness_utxo as for any non-taproot input; ecdsa_sig_hash and finalize refuse it; estimated_input_sizes refuses it unless a sizer answers, where it priced a 64-byte key path signature. A sizer’s answer for a witness_unknown input, bare or wrapped, is its witness stack, where it went into the script_sig.

  • A MuSig2 or FROST session refuses a non-taproot input, or one whose control block does not prove its leaf against the output key the input spends (ISS 820), before round 1 spends a nonce on it.

  • BIP322’s verify and assert_as_valid refuse the text of a simple or full signature over its bound, signatures that verified True included (ISS 744), and a to_sign heavier than MAX_BLOCK_WEIGHT is invalid, so verify answers False for it (ISS 826). A proof of funds over its bound is inconclusive. A Sig built from the payload skips the text bound, and a simple or full one over it is refused by the weight bound.

  • The fetchers’, electrum’s and the descriptor parser’s refusals describe a value the caller passed, or name it (ISS 802, ISS 828, ISS 836, ISS 843, and the issues their pull requests close): a bound by its sign and bit length, a height or a tx id by its parameter’s name, an HTTP error by its endpoint with <txid> or <height> in place of the value. A CompactFilterFetcher label still names a height of the chain it syncs, at most its tip, which a caller’s height can be. An Electrum RpcError carries the server’s code where it fits 16 signed bits, else 0, and the method, and its data is None. electrum.decode_response takes the method its line answers and refuses one the Electrum protocol does not name, and fetcher.block_header_from_raw takes no height. Act on it if you match a refusal’s text or read an Electrum error’s message, data or code.

v2026.11.12

A ledger encrypts its private keys at rest and bumps a transaction’s fee, and its file moves to format 2, which v2026.11.11 cannot read.

  • A ledger’s file is format 2 (ISS 717). ledger.FORMAT_VERSION is 2. A format-1 file, as v2026.11.11 writes it, and a backup of one migrate on load, keeping every source and key. v2026.11.11 refuses a format-2 file as newer. Act on it if you may go back to v2026.11.11: keep a copy of the file from before the upgrade.

  • Ledger.encrypt encrypts a ledger’s private keys under a passphrase (ISS 717), as Core’s encryptwallet does, and leaves the ledger locked. unlock_keys, lock_keys, change_passphrase and unlocked_until follow Core’s walletpassphrase, walletlock, walletpassphrasechange and getwalletinfo. While locked, sign, spend, bump_psbt, bump and add_source of a wallet holding keys are refused. encrypt refuses a source whose scripts need a private key at every index, a hardened /*h, and an encrypted ledger refuses to add one. cryptography 47.0.0 or later is a dependency, except on Windows arm64, where the encryption extra asks for it. Act on it if you keep a ledger: encrypt it, take a backup, restore it and unlock the copy with unlock_keys to check it, and only then destroy the backups taken before, which keep the keys in plain text, as SECURITY.md says. A copy kept to go back to v2026.11.11 holds them in plain text too: keep it where only you can read it.

  • Ledger.bump_psbt and Ledger.bump replace a transaction at a higher fee (ISS 715), as Core’s psbtbumpfee and bumpfee do. fund and spend give each input Core’s sequence 0xfffffffd, where they gave 0, and replaceable=False gives 0xfffffffe, which does not signal BIP125. spend takes comment and comment_to, and WalletTx gains comment, comment_to, replaces and replaced_by, which default to None. Act on it if you need a spend that does not signal BIP125: pass replaceable=False. That does not stop a replacement: a node with full RBF, Core’s default since 28.0, accepts one all the same.

  • An ECDSA sig_hash is refused for a psbt input whose script is a witness program of a version with no rule (ISS 747), as Core does: a version from 2 to 16, or a version 1 program that is not p2tr. ecdsa_sig_hash and PsbtView.ecdsa_sig_hash raise BTClibValueError where they answered the legacy hash, sign skips such an input, and finalize and assert_signatures_only refuse a partial signature on one. Act on it if you call ecdsa_sig_hash on such an input: expect the refusal.

  • A refusal describes the value it refuses by its type and size (ISS 745, ISS 749, ISS 790, ISS 791, ISS 792, ISS 793, ISS 795), so that a key passed in the wrong place is not printed. Refusals of electrum, fetch, psbt, bip322 and silent_payments, among others, change text, as invalid height: a negative int of 1 bits < 0 in place of invalid height: -1. Act on it if you match those messages: match the new ones.

  • PsbtOut.parse refuses a PSBT_OUT_TAP_TREE key with key data (ISS 754), as BIP-174 and Core do. Nothing to act on: such a psbt is invalid.

  • Bolt11Invoice.to_invoice and payee refuse a network BOLT11 has no prefix for with BTClibValueError (ISS 770), on an invoice built with check_validity=False, where they raised KeyError. Act on it if you catch that KeyError: catch BTClibValueError.

CHANGELOG.md’s own v2026.11.12 section has the rest.

v2026.11.11

A fresh install of 2026.11.7 does not work: most of its modules, bip32, psbt and descriptors among them, fail at import. It declares btclib>=2026.10.30 and ecc>=2026.11.2 with no upper bound, so an install resolves ecc 2026.11.8 or later, which has no ecc.ecc, and btclib 2026.11.10, which has no btclib.ecc, btclib.base58 or btclib.bech32. import btclib_wallet succeeds, since the package imports a module only when it is first asked for, and import btclib_wallet.bip32 raises ImportError: cannot import name 'base58' from 'btclib'. This release runs on both.

  • btclib 2026.11.10 and ecc 2026.11.8 are the floors (ISS 729, ISS 720, ISS 735). ecc’s schemes are imported from its top level, from ecc import dsa, and bms from btclib.bms. Act on it if you install 2026.11.7: upgrade, or pin btclib<2026.11.10 and ecc<2026.11.8, with which its modules import. Act on it if you pin btclib or ecc below the floors: raise the pins, and read btclib’s and ecc’s notes, whose breaking changes reach you with them.

  • base58-bech32 is a dependency, at 2026.10.23 or later (ISS 731). The Base58Check and bech32 codecs come from it, and a refusal of theirs that your input reaches leaves as BTClibValueError or BTClibTypeError. A silent-payment address that verifies only as bech32 is refused with invalid checksum: BECH32_1_CONST verifies, and a BOLT11 invoice that verifies only as bech32m with invalid checksum: BECH32_M_CONST verifies, where both said invalid checksum. Act on it if you match that message: match the new ones.

  • The secp256k1 extra needs btclib-secp256k1 0.8.0.13, the first release whose silentpayments.scan_outputs refuses a label tweak outside [1, n-1]. Act on it if you pin the bindings below it: raise the pin.

  • bip322.sign takes a WIF as a str (ISS 733), as btclib.bms.sign does, beside a PrvKeyData. Bytes and an int are refused. Nothing to act on: a call that worked still works.

  • combine and join AND the undefined bits of PSBT_GLOBAL_TX_MODIFIABLE (ISS 686), as Core’s combinepsbt does, a psbt without the field counting as 0. They ORed them. assert_signatures_only accepts an answer that clears an undefined bit the request set, and refuses one that sets an undefined bit the request had clear. Act on it if you set those bits: expect them cleared unless every psbt sets them.

  • EsploraFetcher.get_history reads at most max_pages pages of confirmed transactions a script (ISS 699). A script whose pages do not end within the bound, and a page longer than Esplora’s, is a FetchError, and no history is returned. A mempool transaction the explorer no longer has when it is asked for is skipped. Act on it if a script of yours has a longer history: pass a larger max_pages.

  • btclib_wallet.ledger keeps descriptor wallets’ transactions and coins in a SQLite file (ISS 712). The file holds the private keys in plain text, as do its backups and restored copies, as SECURITY.md says. Act on it if you use a Ledger: keep its file and every backup where only you can read them.

  • fetch.RelayFeeReader reads a node’s relay fee floors, and fetch.start_height_for_time maps a birth time to a height (ISS 711). BitcoinCoreFetcher and BitcoinCoreRestFetcher implement the first.

CHANGELOG.md’s own v2026.11.11 section has the rest.

v2026.11.7

  • bip322 refuses a signature whose legacy or P2SH script runs an OP_IF or OP_NOTIF on an argument other than the empty vector or 0x01 (ISS 493), as BIP322’s required rules ask. It accepted one. REQUIRED_RULES adds btclib’s ScriptFlag.MINIMALIF_ALL, which holds every script to the rule. Act on it if you verify BIP322 signatures and relied on that acceptance, or read REQUIRED_RULES: expect the new flag.

  • btclib 2026.10.30 is the floor, the first release with ScriptFlag.MINIMALIF_ALL. Act on it if you pin btclib below it: raise the pin, and read btclib’s notes for v2026.10.28 and v2026.10.30, whose breaking changes reach you with it.

  • A silent-payment PSBT counts k per scan key over its outputs sorted by spend key, then by output index (ISS 676), the rule of BIP375 as bitcoin/bips#2207 merged it. It counted them in output index order. Where one scan key has several outputs whose spend keys are not in index order, output_scripts and set_output_scripts assign k differently, and assert_output_scripts_as_valid refuses a PSBT built with the old order. Act on it if you build or check such PSBTs: build them again.

  • In one transaction, silent-payment addresses that share a scan key must belong to one recipient (ISS 685). k counts per scan key, and a recipient’s scan stops at the first k it does not find. An address with a recipient’s scan key and a spend key that is neither the recipient’s nor one of its labels, whether a fake address or another owner’s, takes a k in that recipient’s count, and every output of the recipient counted after it stays spendable but is never found. output_keys counts in the order the addresses are given. A PSBT counts in the order of the spend keys, so in a PSBT no order of the outputs protects the recipient. Addresses with different scan keys do not affect each other, and a recipient’s own labelled addresses share its scan key without harm where its scan holds those labels. Act on it if you pay silent-payment addresses: pay two that share a scan key, the first key keys_from_address returns, in one transaction only where you know one recipient owns both and scans for the labels it published, and otherwise in separate transactions.

  • EsploraFetcher, ElectrumFetcher and BitcoinCoreRestFetcher list a script’s history with get_history (ISS 629), as BitcoinCoreFetcher does. What an Esplora or Electrum server answers is unverified beyond each transaction’s id, which fixes all of it but its witness, and the checks each docstring names. Either server can leave a transaction out, list one that touches none of the scripts, change any transaction’s witness, and name a start, a tip or a confirmed transaction’s block, height included, that the best chain does not hold, or report a confirmed one as in the mempool; an Esplora server can also put a transaction in a block, or at a position in it, where it is not. The signatures of the mempool transactions are not verified, and one of them may never confirm. Act on it if you read a history from such a server: check that each transaction pays to one of the scripts or spends an output that does, and verify every witness, every unconfirmed transaction, and the start, the tip and the block of each confirmed one, and from Esplora its position there, before you rely on them. These checks do not find a transaction left out: the history is complete only where you check it another way, such as against a node of your own.

CHANGELOG.md’s own v2026.11.7 section has the rest.

v2026.11.3

  • The silent-payment sender and a labelled address no longer add a secret tweak to a point with tweak_add_var (ISS 591, ISS 640). output_key computes t_k*G with mult and adds B_m with ecc.curves.add, and labeled_address_from_keys does the same with the label tweak and B_spend. On the bindings the libsecp256k1 call behind each is constant time, where tweak_add_var ran the variable-time secp256k1_ecmult. output_keys calls output_key for each output, where the bindings served it through silentpayments.create_outputs, which libsecp256k1 0.8.0 builds with secp256k1_ecmult too. The BIP352 vectors give the same keys and addresses. Act on it if you send silent payments or hand out labelled addresses: install btclib-wallet[secp256k1], since ecc’s Python arithmetic is not constant time either way.

  • ecc 2026.11.2 is the floor, the first release exporting ecc.curves.add. Act on it if you pin ecc below it: raise the pin.

  • bip39-slip39 2026.11.2 is the floor, so mnemonic.electrum.old_master_pub_key_from_mnemonic derives a key from a pre-2.0 Electrum mnemonic whose hex seed has an odd number of characters, as Electrum does, where it refused it (ISS 655). Act on it if you rely on that refusal: expect a key where you caught it. Act on it if you pin bip39-slip39 below 2026.11.2: raise the pin.

  • mnemonic.electrum.old_master_pub_key_from_mnemonic refuses a seed written as hex with whitespace between pairs with a new message, “pre-2.0 electrum seed is not a hex string”, where it said the seed decodes to hex characters “which are not octets”. The exception is still bip39-slip39’s Bip39Slip39ValueError. Act on it if you match that message: match the new one.

CHANGELOG.md’s own v2026.11.3 section has the rest.

v2026.11.2

  • The wallet raises its own FetchError, HttpError, RpcError, SignerError, SignerNotFoundError, InconclusiveError and NoDescriptorError, declared in btclib_wallet.exceptions (issue btclib-org/btclib#2709). It raised btclib’s classes of the same names, from btclib.exceptions. Each derives from the same btclib class as before, BTClibRuntimeError or BTClibValueError, so except BTClibException still catches it. Act on it if you catch one of them imported from btclib.exceptions: import it from btclib_wallet.exceptions. TcpPeerTransport still raises bitcoin_core_rpc.FetchError, as it did before; CompactFilterFetcher raises the wallet’s.

  • The Electrum codec is btclib_wallet.electrum (issue btclib-org/btclib#2709). ElectrumFetcher uses it in place of btclib.electrum, so get_tx_merkle returns a btclib_wallet.electrum.MerkleProof. Act on it if you import btclib.electrum, or check that proof’s type against btclib’s class: use btclib_wallet.electrum.

  • CompactFilterFetcher checks the filter headers it is served against more peers (ISS 352). It takes them as cross_check, none by default, and compares their cfcheckpt and cfheaders with those transport serves, as BIP157’s Client Operation recommends. At the first disagreement it downloads the block, and drops a peer whose filter does not match its own header or leaves out one of the block’s output scripts. Where two filters differ only in the scripts the block’s inputs spend, the call fails with FetchError, naming that peer’s index in cross_check, and drops no peer. transport is a read-only property, the peer that serves the chain: when that peer is dropped, the first peer of cross_check takes its place. Act on it if you assign transport: build a new fetcher. TcpPeerTransport.close raises a failed socket close as bitcoin_core_rpc.FetchError, as its send and receive raise theirs, where it raised the OSError. Act on it if you catch that OSError: catch bitcoin_core_rpc.FetchError.

  • HistoryReader lists the transactions that pay to or spend from some scripts (ISS 629). It is a protocol of btclib_wallet.fetch, beside Broadcaster, FeeEstimator and BalanceReader. BitcoinCoreFetcher implements it as get_history, without the node’s wallet; the other fetchers do not. CompactFilterFetcher.scan_blocks refuses an empty script with BTClibValueError, an empty script to look for, where it looked for it. Act on it if you pass an empty script: leave it out.

  • scan_outputs and scan_transaction_outputs refuse a malformed label (ISS 474). A label that is not 33 bytes, a label tweak that is not 32 bytes in [1, n-1], and labels that are not a mapping are refused with BTClibValueError or BTClibTypeError, whether or not the label matches. scan_outputs, and scan_transaction_outputs without the secp256k1 extra, ignored an unmatched one and raised AttributeError for labels that are not a mapping. With the extra, scan_transaction_outputs refused them already, raising a plain TypeError where it raises BTClibTypeError, a TypeError too. scan_transaction_outputs runs the Python scan with at most three labels or at least ten outputs per label, and otherwise calls the bindings where they serve; scan_outputs always runs the Python scan. Act on it if you pass labels of your own making: pass label_lookup’s map.

  • A tr() branch with any count of subtrees but two reports a fault in the subtrees read before the count (ISS 608). parse reads the first subtree, and the second where a comma follows the first, before it refuses the count, as Core does. A bad key or leaf among them raises its own refusal, such as invalid key expression or tr() leaf is not a valid descriptor function, where it raised a tr() branch takes two subtrees, <n> given. Act on it if you match that refusal: match the new message.

btclib’s main no longer declares the seven exceptions or btclib.electrum, and its next release waits for this one, the first that does not import them from btclib (issue btclib-org/btclib#2709).

CHANGELOG.md’s own v2026.11.2 section has the rest.

v2026.10.31

  • A descriptor no longer writes itself into a psbt: update_psbt_input and update_psbt_output are functions of btclib_wallet.psbt (ISS 620). descriptor.update_psbt_input(psbt, vin_i, index, prv_keys) becomes update_psbt_input(descriptor, psbt, vin_i, index, prv_keys), and the same for update_psbt_output, on every descriptor class: Descriptor, PkDescriptor, PkhDescriptor, WpkhDescriptor, ShDescriptor, WshDescriptor, MiniscriptDescriptor, MultiDescriptor, ComboDescriptor, AddrDescriptor, RawDescriptor, TrDescriptor and RawTrDescriptor. The new Descriptor.expansion(index, prv_keys) returns the Expansion they write. The checks run in this order: the types of the descriptor, the psbt and the map index, each a BTClibTypeError, a map index that is not an int (a bool, or an integer of another type such as numpy’s int64) included; prv_keys and the derivation index; the descriptor’s refusal (combo(), addr() and raw(), or a hardened step without its private key); then the map-index range and the paid script. At v2026.10.29 prv_keys and the derivation index came first, then the map-index range, then a hardened step without its private key, the paid script, and combo(), addr() and raw(). Nothing checked the map index’s type: a bool or a numpy integer in range was taken as that map, and another type raised a TypeError, or a BTClibValueError out of range, at or just after the range check. A psbt that is not a Psbt raised an AttributeError at the same check. DescriptorWallet.update_psbt_input and update_psbt_output call these functions and change the same way, after their own position check. Act on it if you call a descriptor’s update_psbt_input or update_psbt_output: import the function from btclib_wallet.psbt and pass the descriptor first. Act on it if you pass a map index that is not an int: pass an int. Act on it if you catch the BTClibValueError of a float map index, or the AttributeError of a psbt that is not a Psbt: catch BTClibTypeError.

  • miniscript_sizer, satisfaction_sizer and miniscript_solver move from btclib_wallet.descriptors to btclib_wallet.psbt (ISS 620). Act on it if you import them: import them from btclib_wallet.psbt.

  • account_descriptors moves from btclib_wallet.descriptors to btclib_wallet.bip44 (ISS 620). Act on it if you import it: import it from btclib_wallet.bip44.

  • A { in or after a key, timelock or hash fragment changes the refusal (ISS 607). In wsh() and in a tr() leaf, parse cuts such a fragment as Core’s Expr does, counting { as a bracket. The BTClibValueError it raises is the key’s own refusal for a key fragment, such as not a key expression or invalid key expression, where it was unbalanced brackets in <fragment>() or invalid key expression. For older() and after() it is A function is needed within P2WSH in wsh() and tr() leaf is not a valid descriptor function in a tr() leaf, and for a hash fragment invalid <fragment>() hash: hex expected, where each was unbalanced brackets in <fragment>(). Act on it if you match any of these refusals: match the new message.

  • KeyGroup.keys holds the same keys (ISS 620). KeyGroup builds them with key_data_from_bip32_key, the public name of the helper it called before, whose code is unchanged. Nothing to act on.

These are the wallet’s side of bip32 and descriptors leaving for a package of their own (btclib-org/.github#1811): the next releases import them from bip32-descriptors.

CHANGELOG.md’s own v2026.10.31 section has the rest.

v2026.10.29

  • BIP322 finds a proof inconclusive where a taproot input carries an annex (ISS 516). assert_as_valid raises InconclusiveError, taproot annex is reserved for future extensions, and verify returns False, where both accepted the proof as valid. BIP341 reserves the annex for future extensions. Act on it if you verify proofs whose taproot witness carries an annex.

  • A miniscript hash refuses whitespace and anything else that is not hex (ISS 507). sha256(), hash256(), ripemd160() and hash160() refuse whitespace and anything else that is not an even, non-zero count of hex characters, as Core’s IsHex, in a descriptor as in a miniscript, as BTClibValueError naming the fragment: invalid sha256() hash: hex expected. A hash with whitespace inside or around it was read without the whitespace, which changed the descriptor’s checksum. Act on it if you pass such a hash: remove the whitespace. Act on it if you match the refusal of a hash that is not hex: match the new message.

  • CompactFilterFetcher checks each block it downloads with btclib’s Block.assert_valid_contextual at the block’s height (issue btclib-org/btclib#2705). scan_blocks fails with FetchError on these blocks, which it accepted. Below the segwit height, a block with witnesses its coinbase commits to fails as block: unexpected witness. A block with a time lock not yet reached, compared with the parent’s median time past where BIP113 binds, fails as block: non-final transaction: <txid>. Where the block’s height carries the rule, a block whose version is too low fails as block: invalid version (outdated): …, and one whose coinbase names another height as block: invalid coinbase height: …. SECURITY.md says which checks a downloaded block passes.

  • CompactFilterFetcher synchronizes a low-work chain with btclib’s HeadersSyncState (issue btclib-org/btclib#2706). A batch of headers the sync refuses fails the call with FetchError, which does not say which check failed: in the first download as headers <first> to <last> are not a chain the rules allow, and in the second as headers <first> to <last> are not the chain first served. Act on it if you match the messages of those refusals.

  • SECURITY.md verifies a release by this repository’s id (ISS 517). Its command passes --owner btclib-org to gh attestation verify and compares the certificate’s sourceRepositoryIdentifier with the id, where it passed --repo btclib-org/btclib-wallet, which SECURITY.md says refuses every release signed before a rename. Act on it if you verify releases with --repo: run SECURITY.md’s command.

CHANGELOG.md’s own v2026.10.29 section has the rest.

v2026.10.26

  • BIP352 skips a taproot input whose control block has an invalid length (ISS 556). pub_key_from_input returns None for a script-path spend whose control block is not 33 bytes plus a multiple of 32, up to 128 of them, where it returned the input’s output key. BIP341 allows no other length, and bitcoin/bips#2339 has BIP352’s reference implementation skip it.

  • The release’s attestation names its signer workflow at a commit (issue btclib-org/.github#1870). release.yml calls reusable-build.yml of btclib-org/.github at a tagged commit, where it called it at refs/heads/main. Act on it if you verify a release with --signer-workflow naming reusable-build.yml@refs/heads/main: name the workflow without a ref, as SECURITY.md does.

CHANGELOG.md’s own v2026.10.26 section has the rest.

v2026.10.25

  • btclib-wallet requires ecc, the renamed btclib-ecc (ISS 466, ISS 477). It requires btclib>=2026.10.24 and ecc>=2026.10.23, and no longer installs btclib-ecc. The exceptions of the curve package are EccException, EccTypeError, EccValueError and EccRuntimeError of ecc.exceptions, where they were the BTClibEcc* ones of btclib_ecc.exceptions, with no alias. Act on it if you catch one that this package lets through, as BIP322 verification does for a legacy signature whose recovered key is the point at infinity: catch the new name. Act on it if you import btclib_ecc and relied on this package to install it: install and import ecc. Act on it if you set BTCLIB_ECC_NO_LIBSECP256K1 to turn the libsecp256k1 bindings off: only ECC_NO_LIBSECP256K1 does.

  • btclib-wallet requires bip39-slip39 and raises its exceptions (issue btclib-org/bip39-slip39#106). It no longer installs btclib-mnemonics, which bip39-slip39 replaces. The mnemonic functions raise Bip39Slip39ValueError and Bip39Slip39TypeError, which share the base class Bip39Slip39Exception, where they raised BTClibMnemonicsValueError and BTClibMnemonicsTypeError, under BTClibMnemonicsException. Act on it if you catch any of them: catch the new names. Act on it if you import btclib_mnemonics and relied on this package to install it: install and import bip39_slip39.

  • extract_tx refuses a fee above max_fee_rate (ISS 459). The default is DEFAULT_MAX_FEE_RATE, Core’s -maxfeerate of 0.10 BTC per kvB, priced on the final virtual size. A missing utxo now raises even with verify_scripts=False, because the check needs every input’s amount. Act on it if you extract a psbt with a higher fee: pass a higher max_fee_rate, or None to skip the check.

  • PsbtOut.script_pub_key is None where the output has no script (PR 483). It is b"" where PSBT_OUT_SCRIPT is present and empty, which a v2 psbt now keeps through serialization. Its JSON form is null for an absent script. The constructor’s default is None, where it was b"". Act on it if you read the field or its JSON, or clear a v2 script: handle None, and assign None to clear one.

  • BIP322 assert_as_valid refuses more (ISS 502, ISS 493). It refuses a proof whose sigop cost, with to_spend’s, is over MAX_BLOCK_SIGOPS_COST; OP_CODESEPARATOR in a P2WSH script or a tapscript leaf; a psbt whose signed message is not the one verified; and an additional legacy input with no Non-Witness UTXO. Act on it if you verify such proofs: they were accepted, and Core or BIP322 refuses them.

  • CompactFilterFetcher downloads a chain that lacks the minimum work twice (ISS 354), as Core’s headers presync does, so memory is bounded before its work is known. A first synchronization from the genesis block downloads the headers twice. SECURITY.md says what it checks.

  • strip_checksum quotes no checksum (ISS 478). A wrong checksum is “invalid descriptor checksum”. Act on it if you match : <checksum> expected: match the new message, and call add_checksum for the right checksum.

  • Some descriptor refusals change their message (ISS 437, 438, 475, 476). parse reads the keys of a multisig before the structure around it, and names Core’s fault for a refused older() or after() number, for a multi() in tapscript and for a multi_a() in P2WSH. The same inputs are refused; only the messages change. Act on it if you match the text of a refusal: match the new one.

  • A taproot input without PSBT_IN_SIGHASH_TYPE accepts SIGHASH_ALL (ISS 461), and finalize builds the witness of a multi_a leaf (ISS 462).

  • New: EsploraFetcher.get_balance, with Balance and BalanceReader (ISS 458), and ElectrumFetcher.get_block_headers (ISS 330).

CHANGELOG.md’s own v2026.10.25 section has the rest.

v2026.10.18

  • parse and multipath_descriptors refuse a key with a space before or after it (ISS 443), as Bitcoin Core does: “Key is invalid due to whitespace”. Act on it if you pass a descriptor with a space by a key: remove the space.

  • multipath_descriptors reads a descriptor as parse does (ISS 393, ISS 370, ISS 448, ISS 450, ISS 451). It hands every expansion to parse, so it refuses a descriptor whose expansion parse refuses, where it returned the expansion, and it parses a descriptor with no multipath step, where it returned it unread. It takes an optional network; with none, the network of a key is not checked. Where a descriptor has several faults, parse and multipath_descriptors report the one Bitcoin Core reads first, a musig() participant’s fingerprint and origin faults carry the musig(): prefix in both, and parse gives Core’s message for a musig() with a step in both its participants and its path. Act on it if you match the text of a refusal: match the new one. Act on it if you expanded a descriptor parse refuses: write it as Core reads it.

  • CompactFilterFetcher finds the blocks that touch a wallet’s scripts over p2p (ISS 329): it reads the BIP157/158 filters one peer serves and downloads only the blocks whose filter matches.

CHANGELOG.md’s own v2026.10.18 section has the rest.

v2026.10.16

  • btclib-wallet 2026.9.30 to 2026.10.9 fail at import beside btclib-ecc 2026.10.16 (issue btclib-org/.github#1742). import btclib_wallet.bip32 raises ImportError, and so does importing psbt, descriptors or wallet: btclib-ecc 2026.10.16 renames point_from_octets to point_from_octets_var, with no alias. Upgrade to this release, or keep btclib-ecc below 2026.10.16.

  • silent_payments.output_keys returns the keys in the order of the addresses (ISS 415). Through 2026.10.9 it grouped them by scan key, so for [A1, B, A2], A1 and A2 sharing a scan key, key 1 was A2’s and key 2 was B’s. A transaction that paired key i with address i paid B’s amount to A2 and A2’s amount to B. Upgrade. Act on it if you reordered the keys yourself: stop reordering them.

Breaking changes

  • btclib-wallet requires btclib 2026.10.16 and btclib-ecc 2026.10.16 (issue btclib-org/.github#1742). Act on it if you pin either lower. Their own release notes list what else they change.

  • block_header_from_raw takes a required, keyword-only network (ISS 328). It checks the proof of work against that network’s limit, where it used mainnet’s for every network. So the Electrum, Esplora, Bitcoin Core and Core REST fetchers read regtest and signet headers. Act on it if you call it: pass the network’s name.

  • ElectrumFetcher.assert_network raises BTClibValueError for a server of another chain (ISS 341). A mainnet fetcher over a regtest server raised FetchError, which FallbackFetcher falls through. Act on it if you catch FetchError there: catch BTClibValueError.

  • psbt.combine keeps the first psbt’s value for a key that several psbts hold (ISS 325), as Bitcoin Core does. It kept the last one’s. Act on it if a later psbt was meant to win: put it first.

  • A psbt is refused for a BIP32 derivation key Bitcoin Core refuses (ISS 342, ISS 356, ISS 377). In the global map the key is a 78-byte xpub whose public key is a point. In an input or output map it is a 33- or 65-byte public key that is a point. Parsing, validating and serializing refuse any other. Act on it if you build such a psbt: put the key BIP174 names there.

  • psbt_utils.deserialize_map takes a required which (ISS 351): the map’s name, global, input or output, for its refusal. Act on it if you call it: pass the name.

  • multipath_descriptors refuses what Bitcoin Core refuses (ISS 299, ISS 301, ISS 353, ISS 365, ISS 367, ISS 397). It expanded these, and refuses them: a key origin fingerprint that is not 8 hex digits; a step with a repeated value, as <1;1>; a path element that is no index; a key with a [ and no ], or more than one ]; a musig() outside tr() and rawtr(); a musig() with a path and a ranged participant; a hardened item of a step in a musig() path; a multipath step in the argument of a function that takes no key, as older(<1;2>). parse refused each expansion, except those of the repeated value and of older(<1;2>), and the unhardened ones of a hardened musig() step. Act on it if you hold such a descriptor: write it as Core reads it.

  • DescriptorWallet refuses two branches that share a script (ISS 411, ISS 414), however their descriptors are spelled: a key origin added, a child xpub for a derivation step, or a fixed chain that is an index of a ranged one. position_of answered the first branch for both. The constructor derives every chain at index 0, so a chain it cannot derive, such as a hardened step with no prv_keys, is refused there. Act on it if you built such a wallet: give each branch its own chain, and the private keys a hardened step needs.

  • Descriptor.index_of and RangedWallet.position_of refuse a last_index that is not a non-negative integer (ISS 407, ISS 413): a negative one with BTClibValueError, where they answered None, and a str, None, bytes, float or bool with BTClibTypeError. KeyExpression.sec refuses an index that is not an int from 0 to 2**31 - 1, as Descriptor does, a fixed key’s included, and so do Miniscript.script and Miniscript.satisfy through it (ISS 383, ISS 404). Act on it if you pass another: pass an int in range.

  • hwi looks a bare executable name up in the absolute directories of the PATH alone (ISS 416, ISS 422). An empty or relative PATH entry is skipped, and on Windows the current directory is not searched and a bare name with no period is looked for as <name>.exe only. A name with a directory part is checked as the file it names. Act on it if hwi was found another way: put its directory on the PATH as an absolute path, or pass the absolute path.

  • The messages of some refusals change, and print no key bytes (ISS 351, ISS 388, ISS 299, ISS 367, ISS 370, ISS 358, ISS 374): a duplicated psbt map key, the MuSig2 and silent-payment key checks, a partial signature, bip85.mnemonic_from_root_key’s words and lang, an extended key whose public key is not a point, a multipath step glued to a key, and the Esplora and Core fee estimate replies. parse refuses a multipath step in a musig() path as multipath key expression: use multipath_descriptors first. parse and multipath_descriptors report the fault Bitcoin Core reads first in a descriptor with more than one. A refused partial signature or MuSig2 key chains no __cause__, and bip85.mnemonic_from_root_key refuses a words that is not an int with BTClibTypeError, where it raised BTClibValueError. Act on it if you match the old text: match the new one. Act on it if you catch BTClibValueError for words: catch BTClibTypeError too.

New

  • btclib_wallet.dlc builds, signs and verifies Discreet Log Contracts (ISS 331, ISS 332): the funding, execution and refund transactions of dlcspecs, their adaptor signatures, and the oracle announcements and attestations.

CHANGELOG.md’s own v2026.10.16 section has the rest.

v2026.10.9

  • A refused import, a password in an Esplora URL, a key that is not a point, a mistyped extended key and a BIP38 password are no longer quoted (GHSA-8qrf-j8jh-cx23). Releases 2026.9.24 to 2026.10.8 can put a private key, a hash of one or a password in an exception, the Esplora URL from 2026.9.30 (PR 311). Upgrade, and treat a key or password that such an error may have logged or shown as exposed.

  • The messages of those refusals changed. core_import.assert_imported raises import refused for request {position}: error code {code}. EsploraFetcher refuses any @ in base_url, and an invalid port as invalid base_url port. A key that is not a point is refused with its prefix byte only, and a bad checksum or a non-ASCII character in an extended key with fixed text. bip38 refuses a password that is not UTF-8 with BTClibValueError instead of UnicodeEncodeError. An out-of-range derivation index and a master fingerprint of the wrong length are refused without their value. A caller that matches on the old messages, or catches UnicodeEncodeError from bip38, has to change.

  • The btclib floor is 2026.10.8 (ISS 306). Through 2026.10.7, btclib quotes a refused network name whole, so KeyWallet(network=wif) put the WIF in the error (GHSA-c6h8-5hv5-3gv7).

  • Descriptor.expand(index, prv_keys) is new. It returns the scripts and the provider at index, what script_pub_keys and provider return, from one derivation of each key, as Bitcoin Core’s Descriptor::Expand does. A caller that needs both can call it instead of the two, and derive every key once rather than twice (ISS 303). provider called alone also derives each key once now. Nothing is removed or changed in what the existing methods return.

CHANGELOG.md’s own v2026.10.9 section has the rest.

v2026.10.8

  • A refused multipath step in a key origin is no longer quoted. In v2026.10.7, a step such as [deadbeef/<WIF;1>] was put in the error message of descriptors.parse and descriptors.multipath_descriptors, so a private key (WIF) in it was echoed. This release leaves the step out (PR 285). Upgrade if you parse descriptors that may hold a private key and log or show the errors. Treat a key in such an error from 2026.10.7 as exposed (GHSA-62jf-v327-86h5, published after this release).

  • multipath_descriptors and parse refuse a multipath step that is not a whole path element. <0;1>h, <0;1>', 0<0;1>, <0;1>> and <<0;1>, in a key path or a key origin, raise BTClibValueError “Key path value is not a valid uint32” (ISS 280). Bitcoin Core refuses each, with that message except <0;1>> and <<0;1> in a key origin, which it refuses as a multipath step. v2026.10.7’s multipath_descriptors expanded them, except <0;1>> and <<0;1> in a key origin, which it refused as Core does. Write a hardened step as <0h;1h>. A caller that matches on the message of another step in a key origin finds the step gone from it: the message is “Key path value specifies multipath in a section where multipath is not allowed”.

CHANGELOG.md’s own v2026.10.8 section has the rest.

v2026.10.7

  • btclib-wallet requires btclib 2026.10.5, which fixes GHSA-9r97-9x22-2pp4 and GHSA-rw95-w37r-537w: verifying a taproot input took time linear in the transaction’s inputs, and each legacy or segwit v0 signature check computed its hash from scratch. bip322.verify runs that verification on a signature it was given. Upgrading btclib-wallet installs it.

  • request_signatures refuses to send a psbt asking for a sig_hash type other than SIGHASH_ALL or SIGHASH_DEFAULT. It raises on an input asking for SIGHASH_NONE, SINGLE or ANYONECANPAY, before the signer is called; pass the types you accept as allowed_sig_hash_types. SoftwareSigner applies its own allow-list, so to sign such a psbt with it, call its sign_psbt directly with allowed_sig_hash_types.

  • musig2.partial_sign and frost.partial_sign refuse an input without its non_witness_utxo, unless every input is taproot and none asks for ANYONECANPAY (GHSA-v4gq-j2v2-c4jp). Give each input the transaction it spends as its non_witness_utxo. Pass require_non_witness_utxo=False only where you know every witness_utxo amount is right.

  • A psbt updated from a descriptor carries a derivation for every key, as Bitcoin Core’s does. A key the descriptor gives no origin has an entry under its own fingerprint, so hd_key_paths and taproot_hd_key_paths are not empty for such a descriptor.

  • HwiSigner.sign_psbt refuses a psbt asking for a sig_hash type other than SIGHASH_ALL or SIGHASH_DEFAULT. It raises before hwi runs; pass the types you accept as its keyword-only allowed_sig_hash_types. request_signatures does not pass its allow-list on, so to send such a psbt to a device, call sign_psbt directly and hold its answer with assert_signatures_only(psbt, answer) before combine.

  • A ScriptWallet psbt carries a derivation for every key, as a descriptor’s does. A key of a KeyGroup given no origin has an entry under its own fingerprint, so hd_key_paths is not empty for such a group. Of two explicit origins for one key, the first is written, not the last.

  • The attestation bundle is attached as v<version>.intoto.jsonl from v2026.10.4 on, where earlier releases attach v<version>.attestation.jsonl. A script that downloads it by name, or passes it to gh attestation verify --bundle, uses the new name.

  • sign, request_signatures and HwiSigner.sign_psbt raise on an input asking for SIGHASH_SINGLE with no output at its index, whatever allowed_sig_hash_types says. Such a signature commits to no output, so whoever holds it can spend the input to any output. Give the input an output at its index, or ask for another type. finalize still checks a signature of this kind.

  • A regtest silent payment address starts with sprt1, as Bitcoin Core’s regtest requires, where earlier releases give tsp1. keys_from_address reads both, with the network type "test". Make a stored regtest tsp1 address again with address_from_keys(..., "regtest"), and update any tsp1 string a test compares against.

  • Bolt11Invoice.from_invoice refuses malformed or repeated fields. A p, s, h or n of the wrong length, a second p, s, d, h or n, an x, c or 9 starting with a zero word, and an amount with a leading zero raise BTClibValueError, where earlier releases skipped a wrong-length field, read the first of a repeated one, or read an amount without its leading zeros. This includes an invoice that an earlier sign wrote with expiry=0 or min_final_cltv_expiry=0: sign it again.

  • A psbt keeps an explicit sig_hash_type of 0 through serialize and combine. sign refuses an ECDSA input asking for it after a round trip through bytes too, where it signed SIGHASH_ALL. HwiSigner.sign_psbt and request_signatures refuse SIGHASH_DEFAULT on an input not known to be taproot, where a device signed SIGHASH_ALL. To sign such a psbt, ask its creator for SIGHASH_ALL or remove the field.

  • silent_payments.output_keys refuses an address of another network than its network argument, "mainnet" by default, as Bitcoin Core’s decoder does. An sprt, tsp or sp address that is not that network’s raises BTClibValueError, so a caller paying testnet, signet or regtest passes the network too.

  • wallet_policy_descriptor, wallet_policy_address and wallet_policy refuse a template BIP388 does not allow. A top-level multi() or sortedmulti(), and a pk() inside sh(), raise BTClibValueError; so do a top-level pk(), combo() or rawtr() in the first two, where earlier releases gave a descriptor or an address. Read such a descriptor with descriptors.parse.

CHANGELOG.md’s own v2026.10.7 section has the rest.

v2026.10.4

  • Signing refuses a sig_hash type other than SIGHASH_ALL or SIGHASH_DEFAULT unless the caller allows it (GHSA-qq38-77mp-j6wr). sign, SoftwareSigner.sign_psbt, musig2.partial_sign and frost.partial_sign raise on an input asking for SIGHASH_NONE, SINGLE or ANYONECANPAY; pass the types you accept as allowed_sig_hash_types. request_signatures passes none, so call SoftwareSigner.sign_psbt directly to sign such a psbt. A legacy SIGHASH_SINGLE input with no output at its index is refused whatever is allowed, and ecdsa_sig_hash and PsbtView.ecdsa_sig_hash refuse it too. finalize, assert_signed and assert_signatures_only refuse an ECDSA signature other than SIGHASH_ALL on an input without PSBT_IN_SIGHASH_TYPE; set the field on a psbt whose signatures use another type.

  • sign, SoftwareSigner.sign_psbt and Psbt.assert_signable refuse an input without its non_witness_utxo, unless every input is taproot and none asks for ANYONECANPAY (GHSA-v4gq-j2v2-c4jp). Give each input the transaction it spends as its non_witness_utxo. Pass require_non_witness_utxo=False only where you know every witness_utxo amount is right: a wrong one can be spent as fee.

  • btclib-wallet requires btclib 2026.10.4, btclib-ecc 2026.10.2 and btclib-secp256k1 0.8.0.10. btclib 2026.10.4 fixes GHSA-9fr5-46w5-5f9r, which made bip322.verify run for minutes on a crafted signature; 0.8.0.10 fixes GHSA-8h6f-34jj-7p6c, an invalid-curve oracle in silentpayments.scan_outputs. Upgrading btclib-wallet installs btclib and btclib-ecc; the secp256k1 extra installs the bindings.

  • Verifying a release’s attestation names a new signer and the tag. gh attestation verify takes --signer-workflow btclib-org/.github/.github/workflows/reusable-build.yml@refs/heads/main and --source-ref refs/tags/v<version>; SECURITY.md has the command. Earlier releases keep reusable-attest.yml.

  • descriptors.parse refuses a key of another network. A WIF or an extended key whose prefix is not the network’s raises BTClibValueError there, where the WIF was accepted and the extended key was refused only by script_pub_keys(). A caller that parses a descriptor for a network its keys are not of has to pass the right network.

  • bip39.mxprv_from_mnemonic and electrum.mxprv_from_mnemonic refuse a falsy passphrase of another type. An empty bytes, 0, False or [] derived the wallet without a passphrase; pass None or "" for that.

  • A miniscript repeating a public key is refused under different spellings of it. descriptors.parse does so as Bitcoin Core does, except for a key with a hardened step.

  • A malleable miniscript keeps neither “s”, “f” nor “e”. Miniscript.properties drops them, and is_signature_required is false for it.

  • join empties the signatures of a version 0 psbt, and sign updates a version 2 psbt’s modifiable flags. Sign the joined psbt again. join refuses a version 2 psbt signed with anything but SIGHASH_NONE|ANYONECANPAY.

  • extract_tx verifies by default. It runs every input’s scripts and checks that the outputs do not exceed the inputs, so it raises on a psbt it extracted before: one with a bad signature, a missing utxo or outputs above the inputs. Pass verify_scripts=False to extract without the check.

  • join refuses to change the lock time or the tx version of a signed version 2 psbt. The signatures would stop verifying. Join the psbts before they are signed, or give them the same lock time and tx version.

  • Miniscript.has_duplicate_keys, is_sane, is_sane_subexpression and insane_sub are methods. Call them with parentheses; without them, if node.is_sane: is always true, and mypy reports it as truthy-function. Pass prv_keys, the mapping parse fills, to derive a hardened step: descriptors.parse refuses wsh(or_i(pk(XPRV/1h),pk(<the key it derives>))). Without its private key, a key with a hardened step is still compared as written.

  • descriptors.parse refuses a threshold no script can meet. A multi() or sortedmulti() threshold of zero or above its key count, and a multi_a() or sortedmulti_a() one of zero, above its key count, or of more than 999 keys, raise BTClibValueError, where they were stored. A thresh() of more than 1000 arguments is refused too, a limit of this library.

  • A number of over 4300 digits is a BTClibValueError. A derivation index, a threshold, an older() or after() number and a @N of that size raised the built-in ValueError; they are refused at ten digits now. Bolt11Invoice.from_invoice refuses an amount of more than 20 digits, which its amount parser read up to 4300 digits.

  • A prv_keys that is not a mapping raises BTClibTypeError. The descriptor, KeyExpression, Miniscript and DescriptorWallet methods taking it, and descriptors.normalized, check it as parse does, where a string raised AttributeError.

  • SilentPaymentOutput’s repr leaves out prv_key_tweak. The tweak is a secret; read the field, not the repr, where you need it.

CHANGELOG.md’s own v2026.10.4 section has the rest.

v2026.9.30

A binary-string entropy passed as bytes is refused with a BTClibTypeError by mnemonic.entropy’s functions taking one, which read it as the digits it spells; pass the str instead. A mnemonic that is not a str is refused with a BTClibTypeError rather than a bare TypeError or AttributeError.

mnemonic.slip39.master_secret_from_mnemonics and mxprv_from_mnemonics refuse shares passed as an iterable that is not a sequence – a set, a generator, an iterator, dict.keys() – with a BTClibTypeError, where they read it; pass a list or a tuple instead.

mnemonic.slip39.mnemonics_from_master_secret refuses with a BTClibTypeError a groups passed as a set or a dict, and a bool as a threshold, a member count or the iteration exponent, where it read them; pass a list or a tuple of integer pairs, and integers. mnemonic.entropy.bin_str_entropy_from_rolls refuses the same way rolls passed as a set or a generator and a bool as a roll or as bits.

mnemonic.slip39.Share refuses with a BTClibTypeError a value that is not bytes – a bytearray or a list of ints included – and a bool as an integer field or a non-bool as extendable, where it held them; pass bytes, integers and a bool. mxprv_from_mnemonics refuses a falsy passphrase of another type, 0, [] or b"", where it read the empty passphrase; pass None or "". mnemonics_from_master_secret refuses an entropy_source answering anything but bytes or a bytearray of the length asked – a 1-of-1 backup read a list, a memoryview or a draw of another length; return exactly that.

fetch.BitcoinCoreFetcher and fetch.BitcoinCoreRestFetcher refuse with a BTClibTypeError a client that is not a BitcoinCoreRpcClient or a BitcoinCoreRestClient respectively, where a stand-in answering the methods they call worked; pass an instance of that class or a subclass. fetch.ElectrumFetcher refuses a timeout that is no number, a bool, not positive, or above threading.TIMEOUT_MAX, which it handed as given to a transport of the caller’s; pass a positive number of seconds. fetch.EsploraFetcher refuses a base_url carrying a user or a password, or without a host, which a transport of the caller’s received; pass the url without them, and let that transport add the credentials.

tx_builder.build_psbt, coin_selection.select_coins, knapsack and single_random_draw require change_script_pub_key: a call that left it out, and so sent every leftover satoshi to the fee, raises a TypeError; pass the change script, or None for a sweep.

tx_builder.build_psbt refuses with a BTClibValueError a fee above max_fee, 0.10 BTC unless raised, and an output being paid below its dust threshold. It refuses a non-zero lock_time too when every input’s sequence is final: set a non-final PsbtIn.sequence, such as 0xfffffffe, on an input for the lock time to bind.

psbt.sign refuses with a BTClibValueError a psbt paying a silent payment it used to sign: one whose silent payment output has no script yet – run psbt.silent_payments.set_input_share or set_global_share, then set_output_scripts, before signing – one with an input’s sighash other than SIGHASH_ALL, and one whose shares, scripts or modifiable flags psbt.silent_payments.assert_as_valid refuses. psbt.extract_tx refuses what that function refuses, and a silent payment output with no script; check_validity=False extracts as before. psbt.combine refuses two psbts carrying different scripts for one silent payment output.

tx_builder.build_psbt also refuses with a BTClibValueError a fee above what max_fee_rate, 0.10 BTC/kvB unless raised, asks of the estimated virtual size, however far below max_fee it is; pass a higher max_fee_rate where such a fee is meant.

A psbt with an input whose witness_utxo is not, in amount and script, the output of its non_witness_utxo the outpoint names is refused with a BTClibValueError by Psbt.assert_valid, and so by Psbt.parse and every function validating the psbt, where it was accepted and the witness_utxo read; set it to that output, or drop it. bip322.assert_as_valid refuses a proof of funds in which it contradicts the transaction an earlier input carries.

psbt.estimated_input_sizes refuses with a BTClibValueError a psbt_in whose non_witness_utxo is not the transaction the tx_in’s outpoint names, and one whose witness_utxo differs from the output that outpoint names, where it answered an estimate; pass the TxIn the input is spent by, and a witness_utxo equal to that output.

psbt_signer.request_signatures refuses a psbt paying a silent payment that psbt.sign would refuse, before the signer is asked, where it sent it on. psbt.silent_payments counts an input by the script it spends, and refuses with a BTClibValueError a counted input whose key the psbt does not carry where it left the input out: give every p2wpkh, p2pkh and p2sh-p2wpkh input a PSBT_IN_BIP32_DERIVATION whose key the script commits to, and a p2sh input its redeem script. A taproot input whose internal key is BIP341’s NUMS point is counted unless PSBT_IN_TAP_MERKLE_ROOT or a control block proves it. set_output_scripts refuses until every counted input has a share, where it derived the scripts from the shares there were.

hwi.HwiSigner.sign_psbt called directly refuses such a psbt the same way, before hwi runs, where it sent it to the device; and it refuses a request that is no Psbt with a BTClibTypeError rather than an AttributeError.

psbt.combine refuses with a BTClibValueError psbts whose merge Psbt.assert_valid refuses, where it returned that psbt; drop from the copies the field that contradicts the others – a witness_utxo differing from another copy’s non_witness_utxo output – before combining. It refuses with a BTClibTypeError a psbts that is not a sequence – a generator, an iterator – where it read it; pass a list or a tuple.

psbt.join refuses with a BTClibTypeError a psbts that is not a sequence – a dict view, any container that is only iterable – where it joined it; pass a list or a tuple.

descriptors.KeyExpression refuses with a BTClibValueError an extended private key as xkey, where it held it; pass its xpub, and the xprv under that xpub in prv_keys where a hardened step needs it.

descriptors.KeyExpression’s xkey holds an extended public key of a version btclib decodes, a wallet-policy placeholder wallet_policy writes – @N, @N/**, @N/<M;N>/*, musig(@N,...) with one of those suffixes, K – or nothing. Any other str is refused with a BTClibValueError and any other type with a BTClibTypeError, where they were held; pass an xpub.

silent_payments.keys_from_address refuses with a BTClibValueError a mixed-case address and one holding a character outside ASCII, where it read the address the string lowers to; pass the address all lower case or all upper case. For such a spelling of an address a wallet handed out, address in wallet answers False and Wallet.address_info raises, where both found the address.

silent_payments.keys_from_address and silent_payments.output_keys, which reads each address through it, BIP32KeyData.b58decode and what reads an extended key through it, bip32.derive among them, bip322.Sig.b64decode, bip322.verify and assert_as_valid for the signature, Psbt.b64decode, tx_or_psbt.tx_or_psbt_from_any, BIP32KeyOrigin.from_description and bip32.str_from_der_path’s fingerprint strip only space, tab, newline, carriage return, vertical tab and form feed. Text padded with U+00A0, U+3000, U+2028, U+0085 or another character outside ASCII that str.isspace counts is refused with a BTClibValueError, where it was read as the value it wraps, and so is text padded with U+001C to U+001F. For such a spelling of an address a wallet handed out, address in wallet answers False and Wallet.address_info raises. Strip that padding before passing the text.

mnemonic.entropy.bin_str_entropy_from_int refuses with a BTClibValueError a string entropy that is not ASCII digits – binary after 0b, hex after 0x, decimal otherwise – once space, tab, newline, carriage return, vertical tab and form feed are stripped around it, where int read 0x1_0, 1_6, +16 and fullwidth or Arabic-Indic digits as 16 and U+00A0 or U+3000 was stripped too. The functions reading a binary-string entropy, bin_str_entropy_from_str, bytes_entropy_from_str and bin_str_entropy_from_entropy among them, refuse one holding anything but ASCII 0 and 1 – a sign, 0b, whitespace, an underscore – which int read as binary; bytes_entropy_from_str raised a bare OverflowError on a leading -. Pass ASCII digits alone. The refusals read invalid entropy: what follows 0x is not ASCII hex digits and invalid entropy: not ASCII decimal digits, where they said not a base 16 number and not a base 10 number, and a negative entropy’s reads negative entropy, without the number. bin_str_entropy_from_wordlist_indexes and bin_str_entropy_from_rolls name the range and not the value of an index or a roll outside it: invalid index: not in [0, 2048), invalid roll: not in [1-6]. Match invalid entropy, negative entropy, invalid index and invalid roll alone.

descriptors.parse refuses with a BTClibValueError a bare multi() or sortedmulti() of more than three keys, one inside sh() whose redeem script is over 520 bytes – sixteen compressed keys are 547 – and one of more than twenty keys anywhere, where it parsed them; put the keys of the first two inside wsh() or sh(wsh()) instead. wallet_policy_descriptor refuses the same, the descriptor it builds being read by parse.

Psbt.b64decode refuses with a BTClibValueError text holding a character outside the base64 alphabet – a line break, a space, U+001C, ! – anywhere past the ASCII whitespace it strips from either end, where it decoded the psbt the rest of the text spells. Remove the line breaks from a wrapped psbt, or pass it to tx_or_psbt.tx_or_psbt_from_any, which removes them.

bip32.indexes_from_der_path, hardenings_from_der_path, int_from_index_str and every function reading a path string through them, bip32.derive and BIP32KeyOrigin.from_description among them, refuse with a BTClibValueError a step whose number is not ASCII decimal digits – 1_0, +1, -0, fullwidth or Arabic-Indic digits – or that is padded with anything but space, tab, newline, carriage return, vertical tab and form feed, where it was read as the number. With bip380_enforced=True they refuse a step padded at all, and so descriptors.parse refuses a step with a space around it. BIP32KeyOrigin.from_description refuses text whose ninth character is not /, where it dropped that character and read deadbeef0/1 as deadbeef/1. int_from_index_str refuses a step that is not a str with a BTClibTypeError, and reads a step with ASCII whitespace after its symbol, "0h ", which it refused. Pass each step as ASCII digits. The refusals read invalid derivation index: not ASCII decimal digits and invalid index: not below 2**31, where they quoted the step or its number; match those. bip85’s functions write their index into a path string, and refuse a negative one with the first and one of 2**31 or more with the second.

descriptors.parse and miniscript.parse refuse with a BTClibValueError a / followed by no path step, in a key origin, after an extended key or after a musig() – [deadbeef/], xpub.../, xpub...//*, musig(...)/ – where they read the key expression as though that / were absent. Remove the /; the refusal reads invalid derivation index. BIP32KeyOrigin.from_description, whose reading of a path is the lenient one, reads deadbeef/ as deadbeef.

Psbt.b64decode, bip322.Sig.b64decode, and bip322.verify and assert_as_valid for the signature, refuse with a BTClibValueError base64 that is not the text base64.b64encode writes for its bytes, where they read those bytes: a bit set in what the last character leaves over, YR== read as YQ==, and on CPython and PyPy 3.11 an = after a whole group. The refusal reads invalid base64 encoding: not canonical, and bip322.verify raises it for a legacy signature so encoded, where it answered False. tx_or_psbt.tx_or_psbt_from_any refuses such a str as neither hex nor base64, and reads such bytes as a raw transaction, where both were read as the psbt. Pass the text base64.b64encode writes, which is the text Bitcoin Core writes and the only text it reads.

A refusal in mnemonic no longer quotes secret material, so its text changed: an unknown word reads unknown '<lang>' word at position <n>, where it was unknown '<lang>' word: '<word>', and a SLIP-0039 one not in the SLIP-0039 word-list: words at positions [<n>, ...]. A BIP39 checksum reads invalid checksum: <n> words, a SLIP-0039 padding invalid padding: must be all zeros, and an unrecognized Electrum sentence unknown electrum mnemonic version; not in [...]. Match the new text if you match these. mnemonic.mnemonic_from_indexes refuses an index outside the word-list, a negative one included, which read from the end of the list; pass indexes in [0, len(wordlist)). WordLists.langs_of_words refuses a lone str, which it read one character at a time; pass the list of words.

mnemonic.mnemonic_from_indexes and WordLists.langs_of_words refuse with a BTClibTypeError an iterable that is not a sequence – a generator, a set, an iterator – where they read it; pass a list or a tuple. mnemonic_from_indexes refuses too an index that is a bool or an integer of a type other than int, numpy.int64 or an object with __index__ included, where it read it; pass ints. dispatch.seed_type_from_mnemonic and all_seed_types_from_mnemonic refuse a lang that is not a str with a BTClibTypeError, where they answered "" and []; pass the language code.

The BIP39, SLIP39 and Electrum mnemonic schemes are btclib-mnemonics’, a dependency of this package, and btclib_wallet.mnemonic keeps what builds a key from their seed: bip39.mxprv_from_mnemonic, slip39.mxprv_from_mnemonics, electrum.mxprv_from_mnemonic and electrum.old_master_pub_key_from_mnemonic. Every path below stops resolving, and nothing re-exports it; import the name from btclib_mnemonics instead. A refusal those four functions pass on from btclib_mnemonics, of a mnemonic, a share or a passphrase, is that package’s own BTClibMnemonicsTypeError or BTClibMnemonicsValueError, a TypeError or a ValueError and no BTClibException, where it was a BTClibTypeError or a BTClibValueError; catch TypeError and ValueError. electrum.mxprv_from_mnemonic still refuses a 2fa, a 2fa_segwit or an old mnemonic with its own BTClibValueError, and for an old one that refusal now ends use old_master_pub_key_from_mnemonic, where it named old_master_prv_key_from_mnemonic. The paragraphs above that name a function that moved describe it as it is under btclib_mnemonics, where each BTClibTypeError or BTClibValueError they name for it is a BTClibMnemonicsTypeError or a BTClibMnemonicsValueError, and where the entropy conversions they name are private.

  • btclib_wallet.mnemonic.dispatch is btclib_mnemonics.dispatch.

  • btclib_wallet.mnemonic.entropy is btclib_mnemonics.entropy, less the functions the bullets below make private.

  • btclib_wallet.mnemonic.mnemonic is btclib_mnemonics.mnemonic.

  • btclib_wallet.mnemonic’s BinStr is btclib_mnemonics.entropy.BinStr.

  • btclib_wallet.mnemonic’s Entropy is btclib_mnemonics.entropy.Entropy.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_random is btclib_mnemonics.entropy.bin_str_entropy_from_random.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_rolls is btclib_mnemonics.entropy.bin_str_entropy_from_rolls.

  • btclib_wallet.mnemonic’s collect_rolls is btclib_mnemonics.entropy.collect_rolls.

  • btclib_wallet.mnemonic’s Mnemonic is btclib_mnemonics.mnemonic.Mnemonic.

  • btclib_wallet.mnemonic’s WORDLISTS is btclib_mnemonics.mnemonic.WORDLISTS.

  • btclib_wallet.mnemonic’s indexes_from_mnemonic is btclib_mnemonics.mnemonic.indexes_from_mnemonic.

  • btclib_wallet.mnemonic’s mnemonic_from_indexes is btclib_mnemonics.mnemonic.mnemonic_from_indexes.

  • btclib_wallet.mnemonic’s normalize_mnemonic is btclib_mnemonics.mnemonic.normalize_mnemonic.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_bytes is gone, private in btclib_mnemonics: pass the bytes to the scheme’s mnemonic_from_entropy, which reads them.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_entropy is gone, private in btclib_mnemonics: pass the entropy to the scheme’s mnemonic_from_entropy, which reads it.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_int is gone, private in btclib_mnemonics: pass the integer to the scheme’s mnemonic_from_entropy, which reads it.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_str is gone, private in btclib_mnemonics: pass the binary string itself.

  • btclib_wallet.mnemonic’s bin_str_entropy_from_wordlist_indexes is gone, private in btclib_mnemonics: use btclib_mnemonics.mnemonic.mnemonic_from_indexes and the scheme’s entropy_from_mnemonic.

  • btclib_wallet.mnemonic’s bytes_entropy_from_str is gone, private in btclib_mnemonics: int(entropy, 2).to_bytes(len(entropy) // 8, "big") is the conversion.

  • btclib_wallet.mnemonic’s wordlist_indexes_from_bin_str_entropy is gone, private in btclib_mnemonics: use the scheme’s mnemonic_from_entropy and btclib_mnemonics.mnemonic.indexes_from_mnemonic.

  • btclib_wallet.mnemonic.bip39’s entropy_from_mnemonic is btclib_mnemonics.bip39.entropy_from_mnemonic.

  • btclib_wallet.mnemonic.bip39’s lang_from_mnemonic is btclib_mnemonics.bip39.lang_from_mnemonic.

  • btclib_wallet.mnemonic.bip39’s mnemonic_from_entropy is btclib_mnemonics.bip39.mnemonic_from_entropy.

  • btclib_wallet.mnemonic.bip39’s seed_from_mnemonic is btclib_mnemonics.bip39.seed_from_mnemonic.

  • btclib_wallet.mnemonic.electrum’s ELECTRUM_WORDLISTS is btclib_mnemonics.electrum.ELECTRUM_WORDLISTS.

  • btclib_wallet.mnemonic.electrum’s entropy_from_mnemonic is btclib_mnemonics.electrum.entropy_from_mnemonic.

  • btclib_wallet.mnemonic.electrum’s hex_seed_from_old_mnemonic is btclib_mnemonics.electrum.hex_seed_from_old_mnemonic.

  • btclib_wallet.mnemonic.electrum’s lang_from_mnemonic is btclib_mnemonics.electrum.lang_from_mnemonic.

  • btclib_wallet.mnemonic.electrum’s mnemonic_from_entropy is btclib_mnemonics.electrum.mnemonic_from_entropy.

  • btclib_wallet.mnemonic.electrum’s old_master_prv_key_from_mnemonic is btclib_mnemonics.electrum.old_master_prv_key_from_mnemonic.

  • btclib_wallet.mnemonic.electrum’s old_mnemonic_from_hex_seed is btclib_mnemonics.electrum.old_mnemonic_from_hex_seed.

  • btclib_wallet.mnemonic.electrum’s version_from_mnemonic is btclib_mnemonics.electrum.version_from_mnemonic.

  • btclib_wallet.mnemonic.slip39’s Share is btclib_mnemonics.slip39.Share.

  • btclib_wallet.mnemonic.slip39’s master_secret_from_mnemonics is btclib_mnemonics.slip39.master_secret_from_mnemonics.

  • btclib_wallet.mnemonic.slip39’s mnemonic_from_share is btclib_mnemonics.slip39.mnemonic_from_share.

  • btclib_wallet.mnemonic.slip39’s mnemonics_from_master_secret is btclib_mnemonics.slip39.mnemonics_from_master_secret.

  • btclib_wallet.mnemonic.slip39’s share_from_mnemonic is btclib_mnemonics.slip39.share_from_mnemonic.

The bound on a timeout that fetch.ElectrumFetcher refuses is fetch.transport.MAX_TIMEOUT, 2147483 seconds, where the paragraph above names threading.TIMEOUT_MAX; pass at most that many seconds.

fetch.EsploraFetcher refuses at construction, with a BTClibTypeError or a BTClibValueError, a timeout that is no number, a bool, not positive, or above fetch.transport.MAX_TIMEOUT, 2147483 seconds, where its first fetch raised a FetchError and a timeout above that bound reached a transport of the caller’s. fetch.transport.TlsLineTransport refuses a timeout above that bound with a BTClibValueError, where it waited with one up to what the platform’s socket took and failed past it, with an OverflowError or, where the socket’s wait refused it, a FetchError; its refusals read non-numeric timeout: <type> and timeout is not a positive number of seconds up to 2147483, where they quoted the value. Pass a positive number of seconds up to that bound.

CHANGELOG.md’s own v2026.9.30 section has the rest.

v2026.9.24

The first release of btclib-wallet, whose modules leave btclib (issue btclib-org/btclib#2129). A caller importing one of them from btclib installs this package and imports it from btclib_wallet instead, the path below the package unchanged: btclib.bip32 becomes btclib_wallet.bip32, btclib.mnemonic.bip39 becomes btclib_wallet.mnemonic.bip39.

CHANGELOG.md’s own v2026.9.24 section has the rest.