btclib_wallet package¶
Subpackages¶
- btclib_wallet.bip32 package
- Submodules
- btclib_wallet.bip32.bip32 module
BIP32KeyBIP32KeyDatacrack_prv_key_var()derive()derive_()derive_from_account()derive_from_account_()derive_from_account_range()derive_from_account_range_()fingerprint()point_from_xpub()prv_keyinfo_from_xprv()pub_key_derivation_tweaks()pub_keyinfo_from_xkey()pub_keyinfo_from_xpub()rootxprv_from_seed()rootxprv_from_seed_()xpub_from_xprv()xpub_from_xprv_()
- btclib_wallet.bip32.der_path module
- btclib_wallet.bip32.key_origin module
- Module contents
BIP32KeyDataBIP32KeyOriginassert_valid_hd_key_paths()bytes_from_der_path()crack_prv_key_var()decode_from_bip32_derivs()decode_hd_key_paths()derive()derive_()derive_from_account()derive_from_account_()derive_from_account_range()derive_from_account_range_()encode_to_bip32_derivs()fingerprint()hardenings_from_der_path()indexes_from_der_path()int_from_index_str()point_from_xpub()prv_keyinfo_from_xprv()pub_key_derivation_tweaks()pub_keyinfo_from_xkey()pub_keyinfo_from_xpub()rootxprv_from_seed()rootxprv_from_seed_()str_from_der_path()str_from_index_int()xpub_from_xprv()xpub_from_xprv_()
- btclib_wallet.descriptors package
- Submodules
- btclib_wallet.descriptors.descriptors module
AddrDescriptorComboDescriptorDescriptorDescriptorTreeMiniscriptDescriptorMultiAMultiDescriptorPkDescriptorPkhDescriptorRawDescriptorRawTrDescriptorShDescriptorTrDescriptorWpkhDescriptorWshDescriptoraccount_descriptors()add_checksum()at_index()checksum()from_address()miniscript_sizer()miniscript_solver()multipath_descriptors()normalized()parse()satisfaction_sizer()strip_checksum()wallet_policy()wallet_policy_address()wallet_policy_descriptor()
- btclib_wallet.descriptors.key_expression module
- btclib_wallet.descriptors.miniscript module
MiniscriptMiniscript.has_duplicate_keysMiniscript.insane_subMiniscript.is_non_malleableMiniscript.is_saneMiniscript.is_sane_subexpressionMiniscript.is_satisfiableMiniscript.is_signature_requiredMiniscript.is_validMiniscript.is_valid_top_levelMiniscript.is_within_resource_limitsMiniscript.key_expressionsMiniscript.max_exec_stack_itemsMiniscript.max_opsMiniscript.max_stack_itemsMiniscript.max_witness_sizeMiniscript.max_witness_stackMiniscript.mixes_timelocksMiniscript.satisfy()Miniscript.script()
SpendContextfrom_script()parse()reads_back()
- Module contents
AddrDescriptorComboDescriptorDescriptorKeyExpressionMiniscriptMiniscript.has_duplicate_keysMiniscript.insane_subMiniscript.is_non_malleableMiniscript.is_saneMiniscript.is_sane_subexpressionMiniscript.is_satisfiableMiniscript.is_signature_requiredMiniscript.is_validMiniscript.is_valid_top_levelMiniscript.is_within_resource_limitsMiniscript.key_expressionsMiniscript.max_exec_stack_itemsMiniscript.max_opsMiniscript.max_stack_itemsMiniscript.max_witness_sizeMiniscript.max_witness_stackMiniscript.mixes_timelocksMiniscript.satisfy()Miniscript.script()
MiniscriptDescriptorMultiAMultiDescriptorPkDescriptorPkhDescriptorRawDescriptorRawTrDescriptorShDescriptorSpendContextTrDescriptorWpkhDescriptorWshDescriptoraccount_descriptors()add_checksum()at_index()checksum()from_address()miniscript_sizer()miniscript_solver()multipath_descriptors()normalized()parse()satisfaction_sizer()strip_checksum()wallet_policy()wallet_policy_address()wallet_policy_descriptor()
- btclib_wallet.fetch package
- Submodules
- btclib_wallet.fetch.bitcoin_core integration module
- btclib_wallet.fetch.bitcoin_core_rest integration module
- btclib_wallet.fetch.broadcaster module
- btclib_wallet.fetch.decorators module
- btclib_wallet.fetch.electrum module
- btclib_wallet.fetch.esplora module
- btclib_wallet.fetch.fee_estimator module
- btclib_wallet.fetch.fetcher module
- btclib_wallet.fetch.transport module
- Module contents
- btclib_wallet.mnemonic package
- Submodules
- btclib_wallet.mnemonic.bip39 module
- btclib_wallet.mnemonic.dispatch module
- btclib_wallet.mnemonic.electrum module
- btclib_wallet.mnemonic.entropy module
BinStrbin_str_entropy_from_bytes()bin_str_entropy_from_entropy()bin_str_entropy_from_int()bin_str_entropy_from_random()bin_str_entropy_from_rolls()bin_str_entropy_from_str()bin_str_entropy_from_wordlist_indexes()bytes_entropy_from_str()collect_rolls()wordlist_indexes_from_bin_str_entropy()
- btclib_wallet.mnemonic.mnemonic module
- btclib_wallet.mnemonic.slip39 module
- Module contents
BinStrMnemonicbin_str_entropy_from_bytes()bin_str_entropy_from_entropy()bin_str_entropy_from_int()bin_str_entropy_from_random()bin_str_entropy_from_rolls()bin_str_entropy_from_str()bin_str_entropy_from_wordlist_indexes()bytes_entropy_from_str()collect_rolls()indexes_from_mnemonic()mnemonic_from_indexes()normalize_mnemonic()wordlist_indexes_from_bin_str_entropy()
- btclib_wallet.psbt package
- Submodules
- btclib_wallet.psbt.frost module
- btclib_wallet.psbt.musig2 module
- btclib_wallet.psbt.psbt module
KeyManagerPsbtPsbt.assert_signable()Psbt.assert_valid()Psbt.b64decode()Psbt.b64encode()Psbt.estimated_vsizePsbt.estimated_weightPsbt.from_dict()Psbt.from_tx()Psbt.has_sig_hash_singlePsbt.inputs_modifiablePsbt.lock_timePsbt.outputs_modifiablePsbt.parse()Psbt.serialize()Psbt.sort_inputs()Psbt.sort_outputs()Psbt.to_dict()Psbt.to_v0()Psbt.to_v2()Psbt.txPsbt.unique_idPsbt.vsize_estimate()Psbt.weight_estimate()
assert_signatures_only()assert_signed()combine()ecdsa_sig_hash()extract_tx()finalize()join()leaf_script()new_signers()prevouts()sign()single_leaf_key()taproot_sig_hash()
- btclib_wallet.psbt.psbt_in module
- btclib_wallet.psbt.psbt_out module
- btclib_wallet.psbt.psbt_size module
- btclib_wallet.psbt.psbt_utils module
assert_not_a_v2_field()assert_valid_leaf_scripts()assert_valid_musig2_participant_pub_keys()assert_valid_musig2_pub_key()assert_valid_musig2_session_data()assert_valid_psbt_version()assert_valid_redeem_script()assert_valid_sp_scan_key_map()assert_valid_sp_v0_info()assert_valid_taproot_bip32_derivation()assert_valid_taproot_internal_key()assert_valid_taproot_script_keys()assert_valid_taproot_signatures()assert_valid_unknown()assert_valid_witness_script()decode_dict_bytes_bytes()decode_leaf_scripts()decode_musig2_participant_pub_keys()decode_taproot_bip32()decode_taproot_tree()deserialize_bytes()deserialize_count()deserialize_map()deserialize_sized_int()deserialize_tx()encode_dict_bytes_bytes()encode_leaf_scripts()encode_musig2_participant_pub_keys()encode_taproot_tree()parse_leaf_script()parse_musig2_participant_pub_keys()parse_taproot_bip32()parse_taproot_tree()serialize_bytes()serialize_count()serialize_dict_bytes_bytes()serialize_hd_key_paths()serialize_leaf_scripts()serialize_musig2_participant_pub_keys()serialize_sized_int()serialize_taproot_bip32()serialize_taproot_tree()taproot_bip32_from_dict()taproot_bip32_to_dict()
- btclib_wallet.psbt.psbt_view module
- btclib_wallet.psbt.silent_payments module
- Module contents
KeyManagerPsbtPsbt.assert_signable()Psbt.assert_valid()Psbt.b64decode()Psbt.b64encode()Psbt.estimated_vsizePsbt.estimated_weightPsbt.from_dict()Psbt.from_tx()Psbt.has_sig_hash_singlePsbt.inputs_modifiablePsbt.lock_timePsbt.outputs_modifiablePsbt.parse()Psbt.serialize()Psbt.sort_inputs()Psbt.sort_outputs()Psbt.to_dict()Psbt.to_v0()Psbt.to_v2()Psbt.txPsbt.unique_idPsbt.vsize_estimate()Psbt.weight_estimate()
PsbtInPsbtOutPsbtViewassert_signatures_only()assert_signed()combine()ecdsa_sig_hash()estimated_input_sizes()extract_tx()finalize()join()new_signers()prevouts()sign()taproot_sig_hash()
- btclib_wallet.wallet package
- Submodules
- btclib_wallet.wallet.descriptor_wallet module
DescriptorWalletDescriptorWallet.branchesDescriptorWallet.descriptor()DescriptorWallet.from_account()DescriptorWallet.from_descriptor()DescriptorWallet.is_rangedDescriptorWallet.is_watch_onlyDescriptorWallet.position_of()DescriptorWallet.redeem_script()DescriptorWallet.satisfy()DescriptorWallet.update_psbt_input()DescriptorWallet.update_psbt_output()DescriptorWallet.witness_script()
- btclib_wallet.wallet.key_wallet module
- btclib_wallet.wallet.script_wallet module
- btclib_wallet.wallet.wallet module
- Module contents
AddressInfoBIP32KeyWalletDescriptorWalletDescriptorWallet.branchesDescriptorWallet.descriptor()DescriptorWallet.from_account()DescriptorWallet.from_descriptor()DescriptorWallet.is_rangedDescriptorWallet.is_watch_onlyDescriptorWallet.position_of()DescriptorWallet.redeem_script()DescriptorWallet.satisfy()DescriptorWallet.update_psbt_input()DescriptorWallet.update_psbt_output()DescriptorWallet.witness_script()
KeyGroupKeyWalletRangedWalletScriptWalletWallet
Submodules¶
btclib_wallet.bip21 module¶
BIP21 payment URI: bitcoin:<address>[?amount=&label=&message=].
https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
The gap between what a user pastes or scans and the typed surface this library offers. It is pure string handling and it sits above the encodings: the address goes to b32/b58, the amount to amount, the network type to network, and a lightning= parameter to bolt11; nothing else in btclib imports this module, so the dependency graph the README draws gains no edge from it, only the one edge into it.
Four rules carry the whole of it, and each is the thing an implementation gets wrong:
an unknown parameter whose name starts with req- makes the URI invalid, and only those: an unknown parameter without the prefix is ignored. That is the entire forward-compatibility story of the scheme
amount is decimal BTC, not satoshi and never a float
a repeated key is an error, not last-one-wins
label and message are percent-encoded, and a bech32 address is legally uppercase – the QR-code case – so nothing here lowercases what it hands to the address decoders
lightning is the one parameter this module gives its own type rather than leaving in others: a BOLT11 invoice, cross-checked against the address it rides beside wherever the two state the same fact – the network, and the amount where both give one.
- class btclib_wallet.bip21.Bip21(address: str, amount: Any = None, label: str | None = None, message: str | None = None, others: Mapping[str, str] | None = None, lightning: Bolt11Invoice | str | None = None, *, check_validity: bool = True)[source]¶
Bases:
objectA parsed bitcoin: payment URI.
others holds the parameters BIP21 says to ignore: kept rather than dropped, because “ignore” is a rule about not rejecting them, and a caller that recognises one is better served by being handed it. Nothing here treats them as meaningful.
lightning is the one parameter this module does treat as meaningful: a BOLT11 invoice, typed rather than left in others, and cross-checked against the on-chain half of the same request wherever both state a fact – the network, and the amount where both give one. Electrum’s bip21.py does the same pairing, which is why the two land in one change.
- property network_type: Literal['main', 'test']¶
Return “main” or “test”, what the address says about its chain.
Not the network, which this was called and could not deliver: a tb1 address is testnet, signet and testnet4 at once, and a 0x6f base58 one is those three and regtest. “main or test” is the whole of what an address carries – issue btclib-org/btclib#207 – and it is the question a payment URI actually raises, a payer needing to know that a request is not for real bitcoin.
- classmethod parse(uri: str, *, check_validity: bool = True) Bip21[source]¶
Return the Bip21 of a bitcoin: URI.
A str and not the String the octet decoders take: a URI is text, and a BTClibTypeError for what is not it – the rule CONTRIBUTING.md’s “The public surface” states, this parameter declaring one type.
btclib_wallet.bip322 module¶
BIP322 signed messages: a script is satisfied, not a key recovered.
ecc.bms signs with a key and lets the verifier recover it, which is why it can only speak about the addresses that are a public key hash – p2pkh, and by Electrum’s extension the two p2wpkh spellings. A taproot address is a tweaked BIP340 key and a p2wsh address is a hash of a script, and no recovery flag names either.
BIP322 asks the other question. The address becomes the script_pub_key of a virtual output, and the signature is whatever spends it: a witness stack, a whole transaction, or a psbt. Verification is then the script interpreter – script.engine – rather than a key comparison, so every script btclib can run is a script that can sign, multisig and timelocks included. The signature commits to the public key too, which the compact ECDSA of BMS does not.
The two virtual transactions are the whole of the construction:
to_spend pays 0 satoshi to the address, and is spendable by nobody: its single input is the null outpoint of a coinbase, and its script_sig is OP_0 PUSH32 message_hash, the BIP340-tagged hash of the message under the BIP0322-signed-message tag. Message and address are therefore both inside its txid
to_sign spends that output and pays 0 satoshi to an OP_RETURN. Its witness – or script_sig – is the signature
A verifier rebuilds to_spend from the message and the address it was given, so a signature made for another message or another address spends a different output and satisfies nothing. Neither transaction can be broadcast, to_spend’s own input being unspendable.
Three encodings, all base64 with a three-character prefix in front of it, and Sig holds whichever came:
smp, the simple variant: the witness stack alone, which is enough where the rest of to_sign is fixed – native segwit, i.e. p2wpkh, p2wsh and p2tr
ful, the full variant: the whole to_sign transaction, which is what a script_sig (p2pkh, p2sh), a version or a lock time needs
pof, the proof of funds variant: a finalized psbt of to_sign, carrying further inputs the signer also controls, with the utxo of each. Whether those outputs exist and are unspent is the chain’s answer and not this module’s
A signature with no prefix is read as simple, which BIP322 allows for compatibility with the implementations that predate the prefixes. The legacy variant is BMS: assert_as_valid hands a 65-byte compact signature to ecc.bms, and only for a p2pkh address, the BIP restricting it to that one.
Verification answers three states, as the BIP does. Valid is a return; invalid is a BTClibValueError or a BTClibRuntimeError, whatever failed being what it says; and inconclusive is InconclusiveError, which is the state for a signature that today’s rules cannot judge – a to_sign whose version is neither 0 nor 2, an upgradeable NOP, a witness program of a version this library does not know. Each of those classes is a BTClibException, so a single except catches whichever comes. verify collapses all three to a boolean, and an inconclusive signature is not a valid one.
What is enforced is BIP322’s list, through the engine’s own flags: the
consensus rules, then LOW_S, STRICTENC, NULLFAIL, MINIMALDATA,
CLEANSTACK, MINIMALIF and CONST_SCRIPTCODE for the required ones, and the
DISCOURAGE_ family for the upgradeable ones – the backticks because a
name ending in an underscore is a link reference to docutils, which
sphinx runs with -W. The one rule of the list that is not a flag is
“all signatures MUST use SIGHASH_ALL”, which no set of flags can express:
it is a rule about the stack elements the interpreter consumed as
signatures, and which elements those were is not readable from the
witness – the control block of a single-leaf taproot tree is 65 bytes,
exactly the shape of a BIP340 signature with an explicit hash type. So
the engine reports them, through verify_input’s hash_types, and the
rule is enforced over what it reports.
The fourth flow of the BIP is not a signature encoding at all: a multisig signature is coordinated as a psbt, and the psbt says what is being signed through PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09, the global field BIP322 adds to BIP174’s registry. Psbt.signed_message holds it; here to_sign_psbt is the Creator that writes one and signed_message the question a Signer puts to what it received – “is this a BIP322 psbt, and for which message” – so that a device shows “signing message m for address A” rather than “spending 0 satoshi”, which is the promise the field exists to let it keep.
https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
- class btclib_wallet.bip322.Sig(payload: Witness | Tx | Psbt)[source]¶
Bases:
objectA BIP322 signature: what the variant carries, and nothing beside it.
One field, because the variant is not a second fact: a witness stack is a simple signature, a transaction a full one, and a psbt a proof of funds, so variant reads the payload rather than being stored where it could disagree with it.
There is no parse, and that is the format rather than an omission: the three payloads are three unrelated serializations and only the prefix of the text form says which one follows, so b64decode is where a signature is read and b64encode where it is written.
- classmethod b64decode(data: bytes | str | bytearray | memoryview, *, check_validity: bool = True) Sig[source]¶
Return the signature the text encodes, whichever variant it is.
A prefix that is not one of the three is not stripped and not guessed at: it is base64 or it is nothing, and the three characters are then part of the witness stack, which is what refuses it. Absent a prefix the signature is simple, which is what BIP322 says a verifier may assume of the implementations that predate them.
- b64encode(*, check_validity: bool = True) str[source]¶
Return the signature as BIP322 writes it: prefix, then base64.
- btclib_wallet.bip322.assert_as_valid(msg: bytes | str | bytearray | memoryview, addr: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, *, legacy: bool = True) None[source]¶
Refuse a signature that does not spend the address’s own output.
The message and the address rebuild to_spend here, so what the signature is checked against is never what it claims to be: a signature for another message, or for another address, satisfies a script that is not this one.
legacy accepts a BMS signature – the 65-byte compact one, with no prefix – for a p2pkh address, which is the compatibility BIP322 keeps and the only address type it keeps it for. False refuses it, for a caller that wants BIP322 proper and nothing else.
Raises InconclusiveError for a signature that is not invalid and cannot be judged valid; see the module docstring for that state.
- btclib_wallet.bip322.assert_signed_message(psbt: Psbt) bytes[source]¶
Return the message this psbt is the BIP322 challenge of, or refuse.
The Signer’s own question, and it is not “does the psbt carry a message”: a message that the transaction does not commit to is what a device showing “signing message m” would be lying about. So the field is one of five conditions and the other four are the psbt being a to_sign – an input to spend, its outpoint being output 0 of the to_spend this very message and this very challenge script rebuild, and the one output that pays nothing to an OP_RETURN.
The challenge script comes from the psbt itself, which is what leaves the caller nothing to be told: assert_as_valid is handed an address and checks a signature against it, where a Signer has not been told an address and is working out what it would be signing.
- btclib_wallet.bip322.message_hash(msg: bytes | str | bytearray | memoryview) bytes[source]¶
Return the BIP340-tagged hash of the message, under BIP322’s tag.
The message enters as it is: no magic string around it, no length in front of it, and no terminator after it. The tag is what keeps this hash from meaning anything under any other protocol, which is the job BMS gives to its “Bitcoin Signed Message:” envelope.
- btclib_wallet.bip322.sign(msg: bytes | str | bytearray | memoryview, prv_key: PrvKeyData, addr: bytes | str | bytearray | memoryview) Sig[source]¶
Return the BIP322 signature of a message for a single-key address.
The address is the argument and not something worked out from the key, one key owning an address of each type: it is the challenge being signed, and BIP322 has no default for it.
The key is the parsed form, as ecc.bms.sign takes it: the scalar signs and the compression flag beside it says which SEC octets the address was built from. A WIF is read once by b58.prv_key_data_from_wif, and a scalar is stated with PrvKeyData(q, network, compressed).
p2pkh, p2wpkh, p2sh-p2wpkh and p2tr are what one private key satisfies on its own, so they are what this signs; the taproot case is the key path, with no script tree. The variant follows the BIP: simple where the address is native segwit and the rest of to_sign is therefore fixed, full where a script_sig has to be carried.
Any other script – multisig, a script path, a time lock – is a Psbt of to_sign signed and finalized by btclib_wallet.psbt, or a Descriptor.satisfy over the signatures it needs, and then a Sig of what comes out. This function is the case that needs neither.
- btclib_wallet.bip322.signed_message(psbt: Psbt) bytes | None[source]¶
Return the message the psbt signs, or None if it signs no message.
assert_signed_message collapsed to what a signing device does with it: a message to show in place of the spend, or nothing and the spend as usual. None is both “no such field” and “a field the transaction does not bear out”, the second being the one worth an exception, so a caller that has to tell them apart asks the other one and reads what it says.
- btclib_wallet.bip322.to_sign(to_spend_tx: Tx, script_sig: bytes | str | bytearray | memoryview = b'', witness: Witness | None = None, *, version: int = 0, lock_time: int = 0, sequence: int = 0, extra_inputs: list[TxIn] | None = None) Tx[source]¶
Return the virtual transaction that spends to_spend_tx.
The signature is script_sig, witness, or both: what satisfies the challenge script, whichever half of an input carries it.
The three keyword arguments are the fields the full variant may set and the simple variant may not, all three of them 0 there: a version of 2 and a lock time for a CHECKLOCKTIMEVERIFY script, a sequence for a CHECKSEQUENCEVERIFY one. extra_inputs are the outputs a proof of funds shows control of, appended after the one input every signature has.
- btclib_wallet.bip322.to_sign_psbt(msg: bytes | str | bytearray | memoryview, addr: bytes | str | bytearray | memoryview) Psbt[source]¶
Return the psbt a Creator hands the signers of this challenge.
BIP322’s fourth flow: a signature that several keys make together is coordinated as a psbt, so what a Signer receives is the unsigned to_sign rather than a witness stack to fill in. Two fields make it one – the message, in the global PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE, and the output being spent, without which no signature can be made – and signed_message is the question this answers on the other side.
The whole of to_spend goes in as the non-witness utxo rather than its one output as a witness utxo: it is the answer for a challenge script of any type, where a witness utxo is the answer for the segwit ones alone, and the transaction is virtual but it is a transaction. Nothing is signed here, and nothing is finalized: what comes back is what a Signer signs, psbt.sign and psbt.finalize being the roles that follow, and Sig of the finalized psbt the proof-of-funds encoding.
- btclib_wallet.bip322.to_spend(msg: bytes | str | bytearray | memoryview, script_pub_key: bytes | str | bytearray | memoryview) Tx[source]¶
Return the virtual transaction the message and the script commit to.
Nobody can spend it and nobody can broadcast it: its one input is the null outpoint a coinbase carries, and a coinbase is valid in a block and nowhere else. What it is for is its txid, which the message hash in its script_sig and the challenge script in its output both enter – so a to_sign built on this txid is a signature for this message and this address alone.
- btclib_wallet.bip322.verify(msg: bytes | str | bytearray | memoryview, addr: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, *, legacy: bool = True) bool[source]¶
Verify the BIP322 signature of a message for an address.
Raises where the address or the signature is structurally invalid – no address at all, or text no encoding this module reads is written in – and answers False for a well-formed pair that is merely not authentic. See _assert_structurally_valid_.
False for an inconclusive signature as well as for an invalid one: the two states are worth telling apart, and assert_as_valid is where they are, but neither of them is a signature that verified.
btclib_wallet.bip38 module¶
BIP38: a password-protected private key.
The block cipher is supplied by the caller. https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki
A BIP38 record is a 58-character Base58Check string starting “6P”, built from a private key, a password, and scrypt – slow on purpose, so that trying one guessed password costs an attacker what it costs the owner. Two modes:
non-EC-multiply (
encrypt(), anddecrypt()on the result): a known private key is encrypted under a password, and only the same password decrypts it back. The party doing the encrypting sees the key.EC-multiply (
intermediate_code(),new_key_pair(), anddecrypt()on the result): the owner, who holds the password, hands a printer anintermediate_codederived from it, and the printer draws a new key pair and an encrypted record from that code alone, never learning the password or the private key. Only the owner’s password decrypts the record back to the same key. An intermediate code optionally embeds a lot and sequence number (0..1048575 and 0..4095), which lets an owner requesting a batch of keys tell them apart and, later, confirm none was substituted.
decrypt reads the two-byte prefix that opens every record –
0x0142 or 0x0143 – and answers either mode from one call; a
caller does not choose which decryption to run, the record says. What it
returns is a btclib.key.PrvKeyData, the shape a parsed private
key takes everywhere else in this library since issue btclib-org/btclib#1188’s
WIF row landed: this module has no format of its own to hand back, network is
always “mainnet” for it, since nothing in a BIP38 record names one.
Why the cipher is a parameter. BIP38 encrypts with AES-256 in ECB – each 16-byte block on its own, no chaining – and btclib.ecc.ecies’s module docstring has the argument in full for why btclib ships none of its own: a table-driven block cipher leaks its key through cache timing, and shipping one anyway is a worse answer than shipping none. encrypt and decrypt here take encrypt_block / decrypt_block callables for that reason, the same shape ecies.encrypt and ecies.decrypt take for AES-128-CBC. new_key_pair, the printer’s half of EC-multiply, takes encrypt_block too, being the one function of the four that calls the cipher forward rather than back.
The contract those callables must honour. Both are called
positionally, as f(key, block), with a 32-byte key and a 16-byte
block, and return a 16-byte block: no iv, no padding, no chaining –
ECB is the absence of a mode, not one to add, and BIP38 never encrypts
more than two blocks at a time. Anything other than AES-256 will
round-trip against itself and produce a record no other implementation
of BIP38 can read, which is the whole point of the algorithm; the
callables are a way to source AES, not a choice of cipher.
No MAC, unlike BIE1. ecies.decrypt checks an HMAC before it ever calls the caller’s cipher, so a wrong key never reaches it. BIP38 has no such check: decryption runs the cipher first and only then re-derives the Bitcoin address the record’s four-byte addresshash names, so a wrong password still calls decrypt_block before decrypt can tell the password was wrong. SECURITY.md carries this as its own point.
The address hash is always mainnet P2PKH. BIP38 predates every other network this library knows and has one encoding, not one per network: the record’s addresshash is always the double-SHA256 of a mainnet P2PKH address (compressed or not, per the record’s own flag byte), whatever network the caller means to spend the recovered key on. That address is BIP38’s own checksum against the wrong password, not a claim about where the key is used.
- btclib_wallet.bip38.decrypt(encrypted_key: bytes | str | bytearray | memoryview, password: str, decrypt_block: Callable[[bytes, bytes], bytes]) PrvKeyData[source]¶
Return the private key a BIP38 record decrypts to, given the password.
Reads both modes from one call: the record’s own two-byte prefix says whether it is non-EC-multiply or EC-multiply, and this dispatches rather than asking the caller to know which. decrypt_block must be AES-256 as a single block, key then block; the module docstring has the contract in full, including the one way this differs from ecies.decrypt – there is no MAC, so a wrong password still calls the cipher before the mismatch is caught.
Raises NotAPrvKeyError for a prefix no BIP38 record has, InvalidPrvKeyError for a recognised record with an invalid flag byte, and BTClibRuntimeError for a password that does not match – the same two-class split b58.prv_key_data_from_wif makes for a WIF, plus the runtime check a password has and a WIF does not.
- btclib_wallet.bip38.encrypt(prv_key: bytes | str | bytearray | memoryview | int, password: str, encrypt_block: Callable[[bytes, bytes], bytes], *, compressed: bool = True) str[source]¶
Return the BIP38 non-EC-multiply encryption of a known private key.
encrypt_block must be AES-256 as a single block, key then block; the module docstring has the contract in full. compressed decides the public key form the record’s addresshash is checked against, which decrypt reads back off the record and does not need restated.
- btclib_wallet.bip38.intermediate_code(password: str, *, lot: int | None = None, sequence: int | None = None, owner_salt: bytes | str | bytearray | memoryview | None = None) str[source]¶
Return the intermediate code an owner hands a printer, from a password.
lot and sequence are given together or not at all: an owner requesting a batch of keys states both, to tell the resulting keys apart and later confirm none was substituted, and the record carries a bit saying whether it was given either. owner_salt overrides the random one, which is what makes a fixed test vector reproducible – reusing one across two intermediate codes for the same password reuses the whole derivation, so leave it alone outside of tests.
- btclib_wallet.bip38.new_key_pair(int_code: bytes | str | bytearray | memoryview, encrypt_block: Callable[[bytes, bytes], bytes], *, compressed: bool = True, seed_b: bytes | str | bytearray | memoryview | None = None) tuple[str, str][source]¶
Return a new (encrypted_key, address) pair from an intermediate code.
The printer’s half of EC-multiply: draws a fresh key pair from int_code alone and encrypts it, never learning the password or the private key it just created. encrypt_block must be AES-256 as a single block, key then block; the module docstring has the contract in full. seed_b overrides the random key material, which is what makes a fixed test vector reproducible – leave it alone outside of tests, since two calls sharing one reuse the same key.
btclib_wallet.bip44 module¶
BIP44 address: an extended key and m/purpose/coin/account/change/index.
https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
The composition every wallet performs and no single call here did: bip32.derive walks the path, b58 and b32 encode the address, and the purpose level – BIP43’s, the first of the five – says which of the four encodings the path means. Nothing in this module derives or encodes anything itself; what it adds is the mapping that makes a path unambiguous, and the two checks that keep it honest.
The module sits above script, and taproot is the reason: a p2tr address encodes the tweaked output key of BIP341, which script.taproot.output_pubkey computes, and bip32 is below script and may not import it. slip132 sits beside it at the top level for a narrower version of the same shape: it needs b58 and b32, which import bip32, so it cannot live inside the package whose keys it derives addresses from either.
What imports this module is what the mapping and the checks are for, and in one direction only: wallet takes the encoders and the purpose lookup rather than keeping a second copy of either, and descriptors takes the path checks and the same lookup for the account descriptor pair it builds. Neither is imported back – a descriptor is what a wallet exports, and this is what a path means.
- btclib_wallet.bip44.address_from_der_path(xkey: BIP32KeyData | bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview, script_type: Literal['p2pkh', 'p2wpkh-p2sh', 'p2wpkh', 'p2tr'] | None = None) str[source]¶
Return the address of a BIP44 derivation path.
der_path is the whole five-level path, m/purpose’/coin_type’/account’/change/address_index, in any spelling bip32.derive accepts; xkey is the extended key it starts from, which may be the master key or any key already partway down it – an account xpub, typically, the depth saying how much of the path is behind it.
The purpose selects the encoding: 44 is p2pkh, 49 p2wpkh-p2sh, 84 p2wpkh, 86 p2tr. A purpose outside that mapping raises, unless script_type names one of those four encodings, which then overrides the mapping for known purposes too.
The network is the extended key’s own; the coin type has to agree with it, 0 for mainnet and 1 for any test network, or the path and the key are describing different chains and neither wins.
btclib_wallet.bip85 module¶
BIP85 deterministic entropy from a BIP32 keychain.
https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki
One root key, many wallets. A fully hardened path off a BIP32 root reaches a child private key k, and HMAC-SHA512(key=”bip-entropy-from-k”, msg=k) turns it into 512 bits of entropy; the path says which application the entropy is for, and the application takes as many of those bits as it needs and truncates the rest. So one backup stands behind a BIP39 wallet, a Bitcoin Core hdseed and a keychain of its own, none of which shares a key with the others.
The HMAC is what makes the entropy hardened whatever the path was: BIP85 mandates hardened derivation but cannot enforce it, and a child key used both as a key and as entropy would otherwise leak the one through the other.
The module sits at the top level, beside bip44 and slip132, and for the same reason: the applications below need b58 for a WIF, b32 for a bech32-encoded key and mnemonic.bip39 for a sentence, and all of those are above bip32, which btclib/bip32/ may not import back. Nothing in the library imports this module.
entropy_from_der_path is the derivation itself and answers for any path, the applications no function here formats included. Every application BIP85 defines is formatted beside it: 39’ (a BIP39 mnemonic), 2’ (the Bitcoin Core hdseed WIF), 32’ (an xprv), 128002’ (a NIP-19 Nostr nsec), 128169’ (raw bytes, which the BIP calls HEX), 707764’ and 707785’ (a base64 and a base85 password), 89101’ (dice rolls) and 828365’ (RSA).
The last two read BIP85-DRNG-SHAKE256 rather than the 64 bytes: a function whose appetite is not known in advance needs a stream, so the entropy seeds a SHAKE256 one and BIP85DRNG.read squeezes it. RSA is where that matters and where btclib stops: the BIP defines the path and the stream to feed a key generator, not how the primes are found, so rsa_drng_from_root_key hands back the reader an RSA library is to be given – and no two libraries handed the same stream need agree on the key, which is why the BIP publishes vectors for every other application and none for this one.
- class btclib_wallet.bip85.BIP85DRNG(entropy: bytes | str | bytearray | memoryview)[source]¶
Bases:
objectBIP85-DRNG-SHAKE256: the 64 entropy bytes as a stream.
The entropy of a path is 64 bytes and no more, which is not enough for a function whose appetite is not known until it has finished – RSA key generation is the BIP’s example. So the 64 bytes seed a SHAKE256 extendable-output function, and read squeezes as many as are asked for, each call continuing where the last one stopped.
The seed must be exactly 64 bytes, which is what the BIP requires: a shorter one is a different stream that no other implementation reaches, so it is refused rather than padded.
read is shake_256(seed).digest(cursor + num_bytes)[cursor:], which is a squeeze of the whole prefix each time rather than a resumed one – hashlib publishes no incremental squeeze. The output is the same either way, SHAKE256’s output at a given length being a prefix of its output at any greater one, and that identity is also what makes a stream read in small pieces equal to the same stream read in one: bipsea, the reference implementation, reads it the same way.
- btclib_wallet.bip85.base64_password_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, pwd_len: int, index: int = 0) str[source]¶
Return a base64 password, BIP85’s application 707764’.
The path is m/83696968h/707764h/{pwd_len}h/{index}h: all 64 bytes of the entropy are base64-encoded and the leading pwd_len characters are the password. pwd_len is bounded to 20..86 inclusive, and the upper end is what keeps the slice clear of the “=” padding those 64 bytes encode to.
- btclib_wallet.bip85.base85_password_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, pwd_len: int, index: int = 0) str[source]¶
Return a base85 password, BIP85’s application 707785’.
The path is m/83696968h/707785h/{pwd_len}h/{index}h: all 64 bytes of the entropy are base85-encoded and the leading pwd_len characters are the password. pwd_len is bounded to 10..80 inclusive.
The alphabet is the one base64.b85encode writes, which is RFC1924’s; the BIP names no alphabet and its vector is in this one.
- btclib_wallet.bip85.bytes_entropy_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, num_bytes: int = 32, index: int = 0) bytes[source]¶
Return raw entropy bytes, BIP85’s application 128169’.
The path is m/83696968h/128169h/{num_bytes}h/{index}h, and the entropy is truncated to num_bytes, which the BIP bounds to 16..64 inclusive. The BIP calls this application HEX and prints its output as hex; the bytes are what btclib hands back, .hex() being the spelling.
- btclib_wallet.bip85.drng_from_der_path(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview) BIP85DRNG[source]¶
Return the BIP85-DRNG seeded with the entropy of a path.
The path is any BIP85 path, entropy_from_der_path’s own rules applying to it: what this adds is the stream on top of the 64 bytes, for an application that needs more of them than there are.
- btclib_wallet.bip85.entropy_from_der_path(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview) bytes[source]¶
Return the 64 bytes of entropy BIP85 derives for a path.
The path is the whole of it, m/83696968h/{app}h/… as the BIP writes it, and every level must be hardened. Each application truncates what it needs off the front; this is the answer for an application no function here formats, the caller doing the truncation and the formatting.
The root key must be private, hardened derivation having no public form. BIP85 assumes a master root key and this does not check the depth: neither does bipsea, the reference implementation, and a derived key is a legitimate root of a keychain of its own – but it is a different one, so entropy derived from it is reproducible only from that same key.
- btclib_wallet.bip85.mnemonic_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, words: int = 12, lang: str = 'en', index: int = 0) str[source]¶
Return a BIP39 mnemonic, BIP85’s application 39’.
The path is m/83696968h/39h/{language}h/{words}h/{index}h: the entropy is truncated to what a sentence of that many words encodes and handed to BIP39, which appends its checksum. words is one of 12, 15, 18, 21 and 24, and lang one of the ten of BIP85’s Language Table, which are ten of the twelve mnemonic.bip39 writes.
- btclib_wallet.bip85.nsec_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, identity: int, account_index: int) str[source]¶
Return a NIP-19 nsec, BIP85’s application 128002’.
The path is m/83696968h/128002h/{identity}h/{account_index}h: the leading 256 bits of the entropy are the secp256k1 secret key, exactly as in the HD-Seed WIF application above, and NIP-19 bech32-encodes them with the nsec human-readable part – plain bech32, not bech32m, and with no witness-version digit in front of the key the way a segwit address carries one.
identity is an independent, unlinkable Nostr key namespace and account_index a distinct key within it. Both must be 1 or more: the BIP reserves index 0’ of either for a future NIP’s key-management use – proof-of-linkage between an identity’s keys, rotation, revocation – and defines no signing key there, so neither defaults.
A scalar of zero or beyond the curve order is refused rather than encoded, the same hard failure wif_from_root_key documents and the same curve-order footnote this section of the BIP cross-references from the WIF one: at odds of about 2**-127 the answer is for the caller to move to the next index.
- btclib_wallet.bip85.rolls_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, rolls: int, sides: int = 6, index: int = 0) list[int][source]¶
Return dice rolls, BIP85’s application 89101’.
The path is m/83696968h/89101h/{sides}h/{rolls}h/{index}h – the sides before the rolls, where this signature takes the rolls first, a die having a customary number of sides and a session no customary length. Each roll is in 0..sides-1, which is what BIP85 defines and what a caller printing them as a die’s faces adds one to.
The rolls are read off the DRNG rather than off the 64 bytes: enough of them exhaust any fixed entropy, and a trial landing at or beyond sides is skipped rather than folded, so that every face stays equally likely.
Nothing bounds rolls from above here beyond what a path level can hold, and the wait is the caller’s: a session is rolls reads of a stream and takes as long as it takes.
mnemonic.entropy.bin_str_entropy_from_rolls is the other direction, dice into entropy for a wallet that does not exist yet; its docstring says how the two number a die’s faces.
- btclib_wallet.bip85.rsa_drng_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, key_bits: int, key_index: int = 0, sub_key: int | None = None) BIP85DRNG[source]¶
Return the DRNG of an RSA key, BIP85’s application 828365’.
The path is m/83696968h/828365h/{key_bits}h/{key_index}h, with a further {sub_key}h level for the GPG sub-keys the BIP allocates: 0’ encrypts, 1’ authenticates, 2’ signs, and the key at key_index itself is the one that certifies.
What comes back is the stream, not a key: BIP85 says an RSA generator should take the DRNG as its source of randomness and says nothing about how the primes are found, so the key belongs to whatever library is handed this reader. btclib generates no RSA key and the BIP publishes no vector for one.
A GPG key built this way has one more rule the BIP states and this cannot enforce: the creation date must be UNIX Epoch timestamp 1231006505, the fingerprint being a function of it.
- btclib_wallet.bip85.wif_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, index: int = 0) str[source]¶
Return a compressed WIF, BIP85’s application 2’.
The path is m/83696968h/2h/{index}h, and the leading 256 bits of the entropy are the secret exponent: this is the hdseed a Bitcoin Core wallet takes. The network is the root key’s own, as the WIF prefix has to name one.
- btclib_wallet.bip85.xprv_from_root_key(root_key: BIP32KeyData | bytes | str | bytearray | memoryview, index: int = 0) str[source]¶
Return an extended private key, BIP85’s application 32’.
The path is m/83696968h/32h/{index}h, and the 64 entropy bytes are read in the order BIP85 states and BIP32 reverses: the first 32 are the chain code and the second 32 the private key. Depth, index and parent fingerprint are zero, the answer being the root of a keychain of its own.
The version is the network’s own xprv or tprv, which is what BIP85 asks for – a testnet root emits a tprv and nothing else does. It is not the root key’s own four bytes: a SLIP132 yprv says which script type that tree is derived for, and the tree this key roots is a new one no such claim has been made about.
btclib_wallet.bolt11 module¶
BOLT11: Lightning Network payment invoices.
https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
A BOLT11 invoice is bech32 over a human-readable part (network and optional amount) and a data part (timestamp, tagged fields, recoverable ECDSA signature) – every piece of which is arithmetic this tree already has: bech32 for the codec, b32 and b58 for the fallback on-chain address, ecc.dsa for the signature and payee recovery, hashes.sha256 for the message digest.
Explicit `m`, always. bech32.decode’s default picks bech32 against bech32m off the first data word as if it were a witness version (_m_from_wit_ver); an invoice’s first data words are timestamp bits, which is not a witness version at all, so the default would misread most invoices. BOLT11 is bech32 alone – BIP350 (bech32m) postdates it and the BOLT never adopted it – so both directions here call bech32.decode and bech32.encode with m=1 explicitly. The 90-character bound BIP173 addresses carry is not a concern either: bech32.py never enforced it, b32.py does, and this module imports bech32 and not b32’s bound.
The wire format’s own source of truth is `tagged_fields`, in the order the invoice carried them. BOLT11 does not fix an order across different tag types – only same-tag repeats have to be most-preferred first – so two spec-compliant writers can place, say, the description before or after the payment secret. Reproducing an invoice’s own bytes on to_invoice therefore keeps the tagged fields exactly as parsed, unknown tags included, the way bip21.Bip21.others keeps a parameter it does not recognise rather than dropping it. Every other attribute – payment_hash, description, fallback_addresses and the rest – is a property computed from that list, picking the first well-formed occurrence of its own tag: BOLT11’s own “fields which must be ignored” example duplicates p, h, s, n and an f field of unknown version with the wrong length on purpose, and a reader is required to skip each malformed repeat rather than fail the whole invoice over it.
Millisatoshi is a plain `int`, named for its unit rather than a new `btclib.amount` type. The human-readable part’s amount is BTC with a multiplier down to pico-BTC, and the pico rule (the last decimal must be zero) exists to keep the result an integer number of millisatoshi, which is the unit HTLCs are denominated in. A dedicated physical-quantity type is a larger surface than one invoice codec needs on its own; amount_msat is a decision this module states rather than one the maintainer inherits unstated.
Network is the human-readable part’s own prefix, not `b32`’s hrp. BIP173’s per-network bech32 prefixes (“bc”, “tb”, “bcrt”) are what an on-chain address carries; BOLT11 defines its own four – lnbc, lntb, lntbs, lnbcrt – and none of them is “ln” plus that prefix: testnet and signet share “tb” on-chain but not here, where signet is “tbs”. There is no fifth prefix for testnet4: the BOLT does not define one, so an lntb invoice is exactly as ambiguous between testnet and testnet4 as a base58 address sharing their version byte is, and network_from_key_value’s own convention – the oldest of the networks a prefix names – is what this module resolves it to as well: “testnet”, never “testnet4”.
A reader that finds a stated payee never trusts recovery over it. BOLT11 requires the opposite of what would be cheaper: an n field, when present, is verified directly and demands a canonical low-s signature: a high-s signature naming a payee is invalid even though recovery alone would accept it, and ecc.dsa.verify_ accepts both forms with no flag to narrow it, so the low-s check here is this module’s own, not delegated. Only where no n field was stated is ecc.dsa.recover_pub_key_ asked at all, and it accepts either form – which is why the BOLT’s own “public-key recovery with high-S signature” example decodes and the “non canonical signature … with ‘n’ field defined” one does not.
- class btclib_wallet.bolt11.Bolt11Invoice(network: str, timestamp: bytes | str | bytearray | memoryview | int, tagged_fields: Sequence[tuple[int, Sequence[int]]], signature: Sig, recovery_id: bytes | str | bytearray | memoryview | int, amount_msat: bytes | str | bytearray | memoryview | int | None = None, *, check_validity: bool = True)[source]¶
Bases:
objectA BOLT11 Lightning invoice.
tagged_fields is the wire’s own ordered (tag, data words) list, and every other attribute below is a property computed from it – see the module docstring for why the order is not collapsed into named fields. from_invoice parses one; sign builds and signs a new one; to_invoice writes either back out.
- assert_valid() None[source]¶
Refuse an invoice a wallet cannot rely on.
Beyond range and shape, this is where the acceptance criterion lives: the message is recomputed from every other field and the stored signature is checked against it, recovering the payee where none was stated.
An even bit of the 9 field that BOLT9 does not assign fails the invoice, which is BOLT9’s rule for a reader that meets a feature bit it does not know; a feature stated without the features BOLT9 says it depends on fails it too. btclib_wallet.bolt9 carries the table that answers both.
- property description_hash: bytes | None¶
Return the 32-byte h field, None where a d field stands in.
- property fallback_addresses: tuple[str, ...]¶
Return the f fields as addresses, skipping unknown versions.
BOLT11: “a reader MUST skip over f fields that use an unknown version” – versions 19-31, which is what the elif below falls through on without raising.
- property features: int¶
Return the 9 field as a bitfield, 0 where none was stated.
The bitfield, and not a verdict on it: assert_valid refuses an even bit BOLT9 does not assign, and a feature whose own dependencies the vector leaves unset. A caller acting on the invoice checks the assigned bits against what it has itself implemented, btclib_wallet.bolt9.FEATURE_NAMES being that table.
- classmethod from_invoice(invoice: str, *, check_validity: bool = True) Bolt11Invoice[source]¶
Return the Bolt11Invoice of a bech32 BOLT11 string.
A str and not the Octets an octet-stream parser takes: an invoice is text, like Bip21.parse’s own uri.
- property payee: bytes¶
Return the 33-byte compressed payee pubkey, stated or recovered.
A stated n field is verified directly and demands a canonical low-s signature; absent one, the pubkey is recovered from the signature instead, and either form of s is accepted – BOLT11’s own two examples of exactly this pair, see the module docstring.
The recovery id is always the low-s R’s, whichever form of s the wire carries: BOLT11’s “public-key recovery with high-S signature” example is the “please make a donation” example with s replaced by n - s and the recovery id left unchanged, which is malleating s alone rather than a second signature – the same r and the same R, its y merely renamed by which of s and n - s names it. libsecp256k1’s own recoverable-signature module never emits a high-s recid to begin with, so recovering the r and the low-s form of whatever s is stored is what makes both of BOLT11’s examples resolve to the same key, and is not a normalization this module invented for the occasion.
- property route_hints: tuple[tuple[RouteHintHop, ...], ...]¶
Return the r fields, each as an ordered tuple of hops.
- classmethod sign(prv_key: bytes | str | bytearray | memoryview | int, network: str, timestamp: bytes | str | bytearray | memoryview | int, payment_hash: bytes | str | bytearray | memoryview, payment_secret: bytes | str | bytearray | memoryview, *, amount_msat: bytes | str | bytearray | memoryview | int | None = None, description: str | None = None, description_hash: bytes | str | bytearray | memoryview | None = None, expiry: bytes | str | bytearray | memoryview | int | None = None, min_final_cltv_expiry: bytes | str | bytearray | memoryview | int | None = None, fallback_addresses: Sequence[str] = (), route_hints: Sequence[Sequence[RouteHintHop]] = (), features: bytes | str | bytearray | memoryview | int = 0, metadata: bytes | str | bytearray | memoryview | None = None, extra_tags: Sequence[tuple[int, Sequence[int]]] = (), check_validity: bool = True) Bolt11Invoice[source]¶
Build and sign a new invoice.
Exactly one of description and description_hash is required, BOLT11’s own rule; the payee is always stated as an n field, derived from prv_key, rather than left to recovery – a stronger reader check than the BOLT requires and never a weaker one, see the module docstring.
- class btclib_wallet.bolt11.RouteHintHop(pubkey: bytes | str | bytearray | memoryview, short_channel_id: bytes | str | bytearray | memoryview | int, fee_base_msat: bytes | str | bytearray | memoryview | int, fee_proportional_millionths: bytes | str | bytearray | memoryview | int, cltv_expiry_delta: bytes | str | bytearray | memoryview | int, *, check_validity: bool = True)[source]¶
Bases:
objectOne hop of a BOLT11 r field: a private channel to route through.
pubkey is the node id at the start of the channel, the wire’s own convention (04-onion-routing.md’s hop_payload starts each channel from the node the payer already reached) rather than the payee’s.
btclib_wallet.bolt9 module¶
BOLT9: the assigned Lightning feature flags.
https://github.com/lightning/bolts/blob/master/09-features.md
Feature bits are assigned in pairs, the odd bit of a pair optional and the even one compulsory, so a reader that meets a bit it does not know must fail on the even one and may ignore the odd one – “it’s ok to be odd”. Answering that is a lookup in BOLT9’s own assignment table, which is the whole of this module: BOLT11’s 9 field, BOLT1’s init message and BOLT7’s announcements each carry a feature vector, and the rule for reading one is the same in all three.
A bit is known here when BOLT9 assigns it, whichever fields its Context column names. That column says where a feature may appear – 9 is a BOLT11 invoice – and reading it as the criterion refuses invoices the BOLT itself calls valid: its “supports features 8, 14 and 99” example sets two even bits, 8 (var_onion_optin) and 14 (payment_secret), whose Context column is empty.
A bit this table names is one a reader can look up, not one a payer implements. unknown_even_bits is the codec’s half of the rule – an even bit nothing has been told how to support – and a wallet acting on an invoice takes the other half itself, checking the assigned even bits against what it has implemented, with FEATURE_NAMES as its table too.
A feature’s dependencies are the features BOLT9 says a vector setting it must set too, listed in the same table’s Dependencies column, and the BOLT’s own rationale is that setting them is what makes a feature vector well-formed. unmet_dependencies answers that from the table alone, so it is the codec’s half by the same division: nothing about it asks what the reader supports. A feature counts as set where either bit of its pair is, the odd half being the optional way of stating it.
The table is a copy of a document that grows, pinned at the revision tests/_data/README.md records for it beside the vendored files, so a pair assigned upstream after that revision reads as unknown here until the pin moves. .github/workflows/vendored-vectors.yml reads that file weekly and opens an issue where a pin is no longer upstream’s tip.
- btclib_wallet.bolt9.unknown_even_bits(features: bytes | str | bytearray | memoryview | int) tuple[int, ...][source]¶
Return the even bits features sets that BOLT9 does not assign.
Lowest first, and never an odd bit: that is the half a reader may ignore, whether or not the table names it.
- btclib_wallet.bolt9.unmet_dependencies(features: bytes | str | bytearray | memoryview | int) tuple[tuple[int, int], ...][source]¶
Return what features depends on and does not set, lowest first.
Each pair is a feature the vector states and one BOLT9’s Dependencies column requires beside it, transitively: a dependency’s own dependencies come back too. Either bit of a pair states its feature, so the optional half meets a dependency as the compulsory half does.
btclib_wallet.coin_selection module¶
Choose which candidates fund a payment, before tx_builder.build_psbt runs.
build_psbt spends every input it is handed, all of them – tx_builder’s module docstring draws that boundary in as many words. Something upstream of it has to decide which utxos those are, and nothing in the tree did: a caller with a set of unspent outputs and a target picked by hand, and a hand pick gets two things wrong that this module exists to compute instead – change worth less than fee.dust_threshold that should have gone to the fee, and an input whose own weight costs more than it is worth at the chosen rate.
Stateless and caller-driven, the same boundary fee and tx_builder draw for the same reason: no wallet, no utxo database, no node. A Candidate is an outpoint, the TxOut it names, and the weight its input will add – tx.input_weight’s answer, supplied by the caller rather than guessed here, since guessing a script’s future satisfaction is exactly what psbt.psbt_size.SolutionSizer exists to refuse doing blindly. select_coins and each of the three algorithms behind it take those candidates, the outputs being paid, a fee rate, and answer with the ones to spend and what change, if any, is worth creating – build_psbt turns that answer into the funded psbt, and is the authority on the transaction’s own final fee: this module’s change is what the selection expects, priced on its own simpler arithmetic, not a second computation of what build_psbt will price exactly once the psbt exists. A candidate’s own fee is what its own weight costs, at fee_rate; target is that plus what the rest of the transaction costs – tx_builder._target_overhead_vsize, the version, the lock time, the output count and the outputs themselves, build_psbt’s own estimate read off a psbt of no inputs rather than a second copy of its arithmetic, so the two cannot drift apart. Padded by one virtual byte for the segwit marker, present once any selected input carries a witness and absent otherwise – a fact this function, asked before a single input is chosen, cannot know – the pad rounding the estimate up so that a witness-only selection is never short, at the cost of a witness-free one being asked for one virtual byte it will not spend. Padded again for the input count’s own var_int, bounded by the size of the candidate pool rather than by the selection chosen from it – also not yet decided when this is asked, but never larger than the pool it is chosen from – so a pool of 253 candidates or more, where a var_int grows past one byte, is never short either; below that the pad is zero and this changes nothing.
Effective value – an output’s value less the fee its own input costs at the chosen rate – is the quantity every algorithm here selects on, Candidate.effective_value. It is what makes a candidate costing more to spend than it holds fall out of a selection on its own, rather than needing a filter a caller has to remember to write.
The algorithms are Bitcoin Core’s, because they are the ones with published behaviour to check a port against: branch_and_bound searches for a changeless match within a window above the target, sized by the cost of creating and later spending change; knapsack is Core’s original stochastic subset-sum solver; single_random_draw shuffles the candidates and takes them in that order until the target, plus a lower bound on the change worth creating, is covered. select_coins runs whichever of the three the caller names – every one of them, by default – and keeps the result of lowest waste, Core’s own SelectionResult::GetWaste metric: the difference between what each selected input costs now and what it would cost at the long-term rate, plus the cost of the change it creates or, where none is worth creating, the excess dropped to the fee. bitcoin/bitcoin@4ec6ff022a’s src/wallet/coinselection.cpp is where all four are read from.
What Core’s own implementation carries that this one does not, and why: an OutputGroup batching several utxos of one address, ancestor and cluster tracking for mempool policy, a maximum selection weight, and CoinGrinder, the fourth algorithm Core added for the weight-minimising case a -maxtxweight policy asks for. Every one of them is wallet state or mempool policy this module has no access to and the parent issue draws the same line in front of: privacy-aware grouping is Electrum’s coinchooser, named in the issue this module answers as the pluggable policy a caller brings on top rather than the default here.
_branch_and_bound also drops three of Core’s own pruning and ordering refinements. None of them changes which selection the search returns – the search space is still fully explored inside _BNB_TOTAL_TRIES, only in a different order or with a different early exit along the way: Core’s descending comparator breaks a tie on equal effective value by the lower-waste candidate (coinselection.cpp:27-36), where this module’s own sort leaves such a tie in whatever order the pool arrived in; the is_feerate_high && curr_selection_waste > best_waste cut (coinselection.cpp:192-198) abandons a partial selection early once no number of further inputs could beat the best one found at a high fee rate, which this module does not check and instead lets the budget spend on to the same conclusion; and the SHIFT loop’s skip-clone step (coinselection.cpp:243-259) advances past a run of candidates of equal effective value rather than evaluating each, which this module evaluates individually within the same TOTAL_TRIES budget.
Randomness is a parameter, not a call. knapsack and single_random_draw both shuffle, and both take a random.Random instead of reading the module-level random functions, so that a test – or a caller who wants a reproducible selection – seeds it. Bitcoin Core’s own reason is the opposite one: FastRandomContext is unseeded by default because a wallet’s coin selection must not be predictable from the outside. Nothing here waives that for a production caller who passes no seed; random.Random() reads system entropy exactly as the module-level functions do, and is only ever a caller’s explicit choice of engine, never this module’s own hidden one.
RBF, CPFP and cancel-by-double-spend stay out, as the parent issue lists them: each needs the ownership and change metadata of a change this module does not track once select_coins returns it.
- class btclib_wallet.coin_selection.Candidate(outpoint: OutPoint, tx_out: TxOut, weight: int, *, check_validity: bool = True)[source]¶
Bases:
objectOne utxo under consideration for spending: what and how much it costs.
outpoint and tx_out are what any input needs; weight is what only a caller can answer – tx.input_weight given the scriptSig and witness this input’s future satisfaction will carry, the same figure psbt.psbt_size.SolutionSizer computes for an input already claimed by a psbt. Guessing it here from tx_out.script_pub_key alone would be wrong for exactly the inputs SolutionSizer itself refuses to guess: a script of no standard type, or a taproot script-path spend naming a leaf nothing but the caller knows.
- effective_value(fee_rate: FeeRate) int[source]¶
Return the value less what spending this input costs, at fee_rate.
Non-positive for an input whose own weight costs at least what it holds – every algorithm here excludes such a candidate from its search, matching Core’s own precondition that an OutputGroup entering coin selection already has a positive one.
- class btclib_wallet.coin_selection.SelectionResult(selected: tuple[Candidate, ...], change: int, waste: int, algorithm: str)[source]¶
Bases:
objectWhat a selection chose, the change it leaves, and its waste score.
change is 0 where none is worth creating – below fee.dust_threshold for change_script_pub_key, or where the caller named none – in which case the excess this selection leaves over the outputs is dropped to the fee instead, exactly as build_psbt treats a change output it finds itself unable to create.
waste is Core’s SelectionResult::GetWaste: comparable across algorithms and across selections of the same candidates, and it is what select_coins orders its attempts by. It is not a fee – a negative value means the long-term rate exceeds the current one, so that spending these inputs now rather than consolidating them later is a saving instead of a cost.
- btclib_wallet.coin_selection.branch_and_bound(candidates: Sequence[Candidate], outputs: Sequence[TxOut], fee_rate: FeeRate, long_term_fee_rate: FeeRate, change_script_pub_key: bytes | str | bytearray | memoryview | None = None, change_spend_weight: int | None = None, *, dust_fee_rate: FeeRate = FeeRate(sats_per_kvbyte=3000)) SelectionResult[source]¶
Search for a changeless selection, Core’s SelectCoinsBnB.
A depth-first search over the candidates sorted by descending effective value, exploring inclusion before omission: a selection landing within [target, target + cost_of_change] – the window a change output would otherwise have to absorb – is a solution, and the search keeps the one of lowest waste rather than stopping at the first. cost_of_change is 0 where change_script_pub_key is None, which asks this search for an exact match to the satoshi – the window a caller sweeping every last one of a set of candidates to a single output wants.
Raised as BTClibValueError: no combination of candidates reaches target within the window, at this rate.
- btclib_wallet.coin_selection.knapsack(candidates: Sequence[Candidate], outputs: Sequence[TxOut], fee_rate: FeeRate, long_term_fee_rate: FeeRate, change_script_pub_key: bytes | str | bytearray | memoryview | None = None, change_spend_weight: int | None = None, *, dust_fee_rate: FeeRate = FeeRate(sats_per_kvbyte=3000), rng: Random | None = None) SelectionResult[source]¶
Solve subset sum by stochastic approximation, Core’s KnapsackSolver.
Every candidate below target + change_target is a subset-sum candidate; a search of ApproximateBestSubset random restarts looks for the closest sum at or above target, falling back to the single smallest candidate that alone covers target where the search does not do better. change_target is cost_of_change – 0 where change_script_pub_key is None, which asks this search for the smallest excess over target rather than for room to leave change in.
rng seeds the shuffle and the stochastic search; unseeded, a fresh random.Random() reads system entropy, matching Core’s own FastRandomContext default.
Raised as BTClibValueError: no combination of candidates reaches target, at this rate.
- btclib_wallet.coin_selection.select_coins(candidates: Sequence[Candidate], outputs: Sequence[TxOut], fee_rate: FeeRate, long_term_fee_rate: FeeRate, change_script_pub_key: bytes | str | bytearray | memoryview | None = None, change_spend_weight: int | None = None, *, dust_fee_rate: FeeRate = FeeRate(sats_per_kvbyte=3000), algorithms: Sequence[str] = ('bnb', 'knapsack', 'srd'), rng: Random | None = None) SelectionResult[source]¶
Run the named algorithms and return the selection of lowest waste.
algorithms names which of “bnb”, “knapsack” and “srd” to try – every one of them by default, Core’s own policy of running all three and keeping the best. A caller who wants exactly one names it, algorithms=(“bnb”,), which answers precisely as calling branch_and_bound would; the composition here is what spares that caller from also having to catch its own BTClibValueError in the presence of the other two.
Raised as BTClibValueError: algorithms names something other than the three above, or none of the algorithms named finds a selection that reaches outputs at this rate.
- btclib_wallet.coin_selection.single_random_draw(candidates: Sequence[Candidate], outputs: Sequence[TxOut], fee_rate: FeeRate, long_term_fee_rate: FeeRate, change_script_pub_key: bytes | str | bytearray | memoryview | None = None, change_spend_weight: int | None = None, *, dust_fee_rate: FeeRate = FeeRate(sats_per_kvbyte=3000), rng: Random | None = None) SelectionResult[source]¶
Shuffle the candidates and take them in order, Core’s SelectCoinsSRD.
The simplest of the three, and Core’s own fallback where the other two find nothing: a random ordering is accumulated until it covers target plus CHANGE_LOWER and the cost of creating a change output – so that a selection landing just past the target still leaves a change worth creating rather than a handful of satoshi – or, where change_script_pub_key is None, target alone.
rng is the shuffle’s own engine; unseeded, a fresh random.Random() reads system entropy.
Raised as BTClibValueError: the shuffled candidates never reach the target, at this rate.
btclib_wallet.core_import module¶
The requests Bitcoin Core’s importdescriptors takes.
https://github.com/bitcoin/bitcoin/blob/master/src/wallet/rpc/backup.cpp
What a wallet does with the descriptors descriptors.account_descriptors builds: hand them to a node, which then watches every script they describe. A request is one json object per descriptor, and this module is the object – no rpc call and no client, deliberately. The caller already has one, bitcoin-core-rpc being a package of its own that btclib_wallet.fetch.bitcoin_core builds on, and a second way to reach a node would be a second thing to keep working.
import_request is one object; account_import_requests is the pair a BIP44 account is, the receiving chain and the change chain marked internal. That mark is why the pair is a function of its own: change imported as a receiving chain is money the wallet reports as incoming payments, and the mistake is invisible until a balance is wrong.
Every rule Core enforces on a request is enforced here, where the error can still say which field it was rather than arriving as an rpc failure half a rescan later:
the descriptor carries its checksum, Parse being called there with
require_checksum = true;an active descriptor is ranged, an unranged one having no keypool to be the active source of;
a range belongs to a ranged descriptor and to no other;
both ends of a range are what ParseRange and ParseDescriptorRange in src/rpc/util.cpp take: ordered, non-negative, an end below 2**31, and fewer than a million indexes between them;
next_index is inside that range;
a label goes with neither internal nor a range;
a timestamp is a number or the exact string now, which is what GetImportTimestamp accepts and nothing else – “NOW” is not it.
Two things are not written, where HWI’s getkeypool writes them: watchonly and keypool are importmulti fields – the other rpc that dict targets – and importdescriptors defines neither. A descriptor wallet is watch-only by holding no private key, which descriptors.parse guarantees of every descriptor it returns.
A multipath descriptor is not built here either: Core takes one and reads
the second element of a two-element step as the internal descriptor, while
descriptors.parse refuses a <a;b> step outright and
multipath_descriptors is what expands one. So the pair of requests is
what this module has, and it says the same thing in two objects.
Two of Core’s answers are read here too, for the reason the requests are built here: both are knowledge of how the node behaves rather than of the protocol, and neither needs a node to be reached, being a function of a reply the caller already has. assert_imported reads what importdescriptors answered, which reports a refusal inside the reply instead of failing the call. watched_range reads what listdescriptors answered, and widened_range turns it into the range a second import may ask for: Core widens any ranged import to its keypool and then refuses every later one that would narrow what it widened to, so importing the same descriptor twice is idempotent only for a caller that asks for at least what is there already.
- btclib_wallet.core_import.account_import_requests(receive: Descriptor, change: Descriptor, timestamp: int | str = 'now', *, active: bool = True, key_range: tuple[int, int] = (0, 999)) list[dict[str, Any]][source]¶
Return the two requests a BIP44 account is imported with.
The pair descriptors.account_descriptors builds, with the second marked internal: that mark is what keeps a wallet from reporting its own change as incoming payments, and it is the one thing a caller writing the two requests by hand gets wrong.
Both chains or neither: a wallet holding the receiving chain alone cannot recognize the change it makes itself, which is an output it stops seeing rather than a feature it lacks.
- btclib_wallet.core_import.assert_imported(requests: Sequence[Mapping[str, Any]], answers: Sequence[Mapping[str, Any]]) None[source]¶
Refuse an importdescriptors reply that did not honour every request.
Core answers one object per request instead of failing the call, so a request it did not honour arrives as success: false inside what the rpc layer calls a reply – a result, which nothing under the caller has any reason to doubt. Left unread, a wallet goes on watching less than its owner believes it does, and the first thing to say so is a balance short of a deposit.
The request is what names the failure: an answer carries the error and not the descriptor it was for, so the two are read in step, and a reply of the wrong length is itself a node that did not answer this.
warnings is not read, that being what Core says about a request it did honour – “Range not given, using default keypool range” is the one DEFAULT_RANGE exists to avoid – and neither is a refusal turned into a value error: the request was one Core parsed, and what it refused is the state of a wallet, which no argument of the caller’s spells.
- btclib_wallet.core_import.import_request(descriptor: Descriptor | str, timestamp: int | str = 'now', *, internal: bool = False, active: bool = True, key_range: tuple[int, int] | None = (0, 999), next_index: int | None = None, label: str = '') dict[str, Any][source]¶
Return the importdescriptors request for one descriptor.
descriptor is a Descriptor or the text of one; either way what the request carries is the checksummed text, which is what Core requires of a descriptor it imports.
timestamp is where the rescan starts, in Unix time, NOW being Core’s own way of saying “do not rescan”: right for a descriptor whose scripts have never been used, and wrong for one being restored, where the time of the wallet’s first use is what finds its history. NOW is the default for the reason there is no default restore date – this module cannot know one, and a silent 0 would rescan the whole chain. A number or NOW itself, and no other string: Core takes those two and refuses the rest, “NOW” included.
internal marks the change chain, which Core then keeps out of what it reports as incoming payments.
active makes the descriptor the wallet’s source of new addresses for that output type and externality, which is what an import for spending wants and a bare watch of some scripts does not. Core requires an active descriptor to be ranged, so this does too.
key_range is the inclusive pair Core takes, both ends included – Core adds one to the second itself. None leaves the field out, which is what an unranged descriptor takes. Ordered, non-negative, an end below 2**31 and fewer than a million indexes wide, which are ParseDescriptorRange’s bounds.
next_index is where an active ranged descriptor hands out its next address, and has to be inside the range, as Core checks.
label names the address, and Core allows one only for a single unranged receiving descriptor: not for change, and not for a range.
- btclib_wallet.core_import.watched_range(descriptor: Descriptor | str, reply: Mapping[str, Any]) tuple[int, int] | None[source]¶
Return the range of a descriptor a wallet watches, None for none.
reply is what listdescriptors answered, which is a wallet’s account of itself: one entry per descriptor it holds, echoing the expression it was imported with and the range it ended up with. This is the question a caller has to ask before importing a descriptor a second time, Core refusing an import that would narrow that range, and widened_range is what the answer is for.
None is a descriptor the wallet does not hold, and equally one it holds unranged: an entry with no range watches one script and has no index to be widened from, which is the same “nothing to include” to whoever is building the next request.
The union of the entries where a wallet holds the same expression more than once – the same descriptor imported as both the receiving and the change chain, which Core allows and which listdescriptors distinguishes only for an active descriptor, internal being defined for those alone. Asking for the union is accepted for either of them, where asking for one entry’s range can be a narrowing of the other’s.
- btclib_wallet.core_import.widened_range(wanted: tuple[int, int], watched: tuple[int, int] | None = None) tuple[int, int][source]¶
Return the range to import: the one wanted, and never a narrower one.
Core widens every ranged import to its keypool – next_index plus a thousand scripts by default, whatever the request asked for – and then refuses any later import that would narrow what it widened to: “New range must include current range” is what the second request is answered, and the whole import fails on it. So a caller whose range has grown asks for the union of the two, which is this, and importing a descriptor again is idempotent because of it.
watched is what watched_range read of the wallet, and None is the descriptor it does not hold yet: DEFAULT_RANGE stands in for it, that being what Core widens a first import to anyway – asking for it makes the reply state which indexes were imported instead of leaving a caller to assume them. A node whose keypool is not the default needs no allowance here: whatever it widened to is what the next listdescriptors says, and the union with that is what the next import asks for.
What the widening costs is worth stating where it is decided: a wallet watches every script of the range, so the addresses past the ones a caller meant are the node’s too, and money paid to one of them is money that wallet reports. An import of exactly what is meant, and no keypool, is what an unranged descriptor per script is for.
btclib_wallet.hwi module¶
A PsbtSigner over Bitcoin Core HWI’s JSON command line.
https://github.com/bitcoin-core/HWI
HWI speaks to Trezor, Ledger, KeepKey, Digital Bitbox, Coldcard, BitBox02 and Jade over HID, USB, serial and emulator transports, and publishes a JSON command line so that other software need not. This module is that other software: it runs hwi as a subprocess. Five of its commands answer the contract btclib_wallet.psbt_signer defines; a sixth, registerdescriptor, is wrapped beside them with no protocol of its own to answer – registering a policy is not a question psbt_signer asks.
Nothing is imported from hwilib, and nothing has to be installed for btclib to work. HWI declares hidapi, libusb1, cbor2, pyserial, noiseprotocol, protobuf and vendor libraries, and a Python range narrower than btclib’s; a mandatory dependency on that is the thing issue btclib-org/btclib#381 rules out. What this module needs at runtime is an executable, named by the caller and absent until a device is actually being used, so the import costs nothing outside the standard library and the tests run with no HWI at all.
An optional extra importing hwilib beside this was weighed and refused (btclib-org/btclib#469), and the reason is that Python range: HWI declares ^3.9,<3.13, where this library’s requires-python is >=3.11, so an extra nobody can install on 3.13 or later is a second and narrower support matrix rather than an option. A subprocess has no such problem, the executable living in an environment of its own. What a caller who does hold an open hwilib device writes instead is a psbt_signer.PsbtSigner of their own: that contract names an in-process driver as one of its shapes, and it is met in the caller’s environment rather than in this one.
enumerate_devices is the one call that names no device; everything else is HwiSigner, which is selected by fingerprint and passes –fingerprint to every command it runs. That is issue btclib-org/btclib#381’s own rule, and the reason selection is not optional: HWI’s –device-type connects to “the first device of this type enumerated”, so two devices of one vendor make which one signs a question of enumeration order.
Both take a network: str, and both put it through network.validated_network_name, which normalizes it with the strip().lower() tolerance issue btclib-org/btclib#216 decided to keep: a name the rest of the library resolves is resolved here, and one no network answers to is refused in the same words whichever of the two took it.
The subprocess is bounded twice. timeout is how long a command may run – a device waiting for a button press is the ordinary case, so the default is generous and a caller signing unattended should lower it – and max_output is how much of its answer is accepted: HWI answers with one json object, and a backend that sends megabytes is one whose output is not read to the end. It bounds stdout and stderr separately, and it bounds what is written rather than what is parsed: both streams go to temporary files whose size is watched while the child runs, so a backend past the limit is killed where it stands rather than read to EOF into memory and measured afterwards.
Failures come back as exceptions.SignerError, carrying HWI’s own error code where there is one: -14 is the user pressing the button that says no, -3 is a cable, -9 is a model that will never do it. The one failure that is not about a device – the executable is not installed – is the SignerNotFoundError subclass, so that a caller which also offers signers of other kinds can tell “no hardware here” from “the hardware could not be reached” without matching on the text of a message. is_available is that one question asked before anything is run, which is when a caller deciding what to offer at all has to have the answer: a refusal is the right answer to a question that was asked, and the wrong way to find out there was nothing to ask.
## Wallet policies
A Ledger will not display or sign a multisig it has not been shown first. BIP388 wallet policies are how it is shown, and registration is a one-time exchange ending in an HMAC the host keeps and replays on every later call. hwilib/_cli.py exposes registerdescriptor for that exchange, and HwiSigner.register_descriptor wraps it the way getxpub and signmessage are wrapped: one request, one opaque answer, nothing here to check it against.
No HWI release through 3.2.0 carries that subcommand – it is on master – and .github/workflows/integration-hwi.yml installs the release its HWI_VERSION names, one of those, so the weekly integration-hwi job runs a command line register_descriptor cannot reach. Raising that value to the first release whose hwilib/_cli.py adds registerdescriptor is what ends the wait, and what makes this paragraph removable.
displayaddress’s BIP388 policy mode – –registration, –index, –multipath-index – is HwiSigner.display_policy_address, on the same unreleased footing as register_descriptor: no release through 3.2.0 carries it either, so it runs against master and against the stand-in tests/hwi_test.py writes, never against the pinned release. psbt_signer.WalletPolicyAddressDisplay/display_policy_address are the protocol and the check beside AddressDisplay/display_address: descriptors.wallet_policy_address computes the address a policy describes at an index and a multipath index, from the template and key-information vector descriptors.wallet_policy writes when the policy is built, and display_policy_address compares it with what the device shows the way display_address compares a device’s screen with a plain Descriptor (issue btclib-org/btclib#1588).
Staying aligned with a project this does not import is two things, and neither is a copy of it. tests/hwi_test.py writes out the surface used – the commands, the flags, the answer keys, the error codes – and tests/_data/README.md pins hwilib/_cli.py and hwilib/errors.py to the revisions it was read from, so the weekly upstream re-check reports a command line that moved. What that already caught: signtx answers signed beside the psbt, which sign_psbt now holds the two strings to.
- class btclib_wallet.hwi.HwiDevice(type: str, model: str, path: str, fingerprint: bytes | None = None, needs_pin_sent: bool = False, needs_passphrase_sent: bool = False, error: str = '', code: int | None = None)[source]¶
Bases:
objectOne entry of what hwi enumerate answers.
fingerprint is None for a device that cannot be asked for one yet – a locked Trezor, a Ledger with no app open – and error says why, with HWI’s own code beside it. Such a device is enumerated on purpose: what a caller does about a locked device is unlock it, and a list that left it out would say it is not there.
- class btclib_wallet.hwi.HwiSigner(fingerprint: bytes | str | bytearray | memoryview | None = None, *, executable: str | Sequence[str] = 'hwi', network: str = 'mainnet', timeout: float = 120.0, max_output: int = 1048576, emulators: bool = False, capabilities: SignerCapabilities = SignerCapabilities(taproot=False, musig2=False))[source]¶
Bases:
objectOne device, selected by fingerprint, answering the signer contract.
btclib_wallet.psbt_signer’s three protocols over five HWI commands: getxpub, signtx, signmessage, displayaddress, and enumerate for the selection. Everything a caller should check about the answers is checked by the functions of that module – request_signatures, display_address, sign_message – and not here: this is the transport, and a transport that also decided what to trust would be two things. register_descriptor is a sixth command with no protocol of its own; the module docstring’s “Wallet policies” says why.
The fingerprint is what a device is named by. Passing one selects it; passing none enumerates and refuses unless exactly one device is usable, because “the first one enumerated” is not a choice a library makes for a caller holding two.
capabilities is the caller’s word: HWI’s JSON CLI does not report what a model supports, and the matrix that does is maintained per vendor and per firmware in HWI’s own documentation. A default of nothing supported is the honest one, and a caller that knows its device says so.
- property capabilities: SignerCapabilities¶
Return what the caller said this device can be asked to sign.
- close() None[source]¶
Refuse further commands; there is no connection to release.
A subprocess per command is what a command line is, so nothing is held open between two of them and closing is a decision rather than a release. Making it refuse afterwards is what gives a caller the same shape as a signer that does hold something – and contextlib.closing then works over either.
- display_address(descriptor: Descriptor, index: int = 0) str[source]¶
Return the address the device shows: HWI’s displayaddress.
The descriptor is sent with the index written into it rather than as the ranged one it may be: HWI derives a ranged descriptor at index 0 whatever was meant, so a caller asking for index 5 would be shown index 0 and told it was 5. descriptors.at_index is what names the one script, and psbt_signer.display_address is what then compares the answer with the address that descriptor describes.
Checksummed, which is what HWI’s –desc parser requires of anything it is given.
- display_policy_address(registration: str, index: int = 0, multipath_index: int = 0) str[source]¶
Show one address of a registered policy: HWI’s displayaddress.
The registered-policy half of displayaddress, mutually exclusive with the plain –desc display_address sends: registration is what register_descriptor returned, and index/multipath_index are what psbt_signer.display_policy_address also resolves descriptors.wallet_policy_address at, on the host side, to check this answer against.
No HWI release through 3.2.0 has this command’s policy group either, so it needs the same master build register_descriptor does; the module docstring’s Wallet policies says what ends that.
- property master_fingerprint: bytes¶
Return the fingerprint this signer was selected by.
Not asked of the device again: it is what every command carries as –fingerprint, so HWI has refused to talk to a device answering anything else before any of them ran.
- register_descriptor(name: str, descriptor: Descriptor) str[source]¶
Register a wallet policy with the device: HWI’s registerdescriptor.
A Ledger will not display or sign a multisig it has not been shown first (module docstring, “Wallet policies”); this is that showing. What comes back is opaque – an HMAC on Ledger, nothing at all on a device that needs none – and is the caller’s to persist and pass back as registration to display_policy_address.
The descriptor goes out ranged, whole rather than at one index: registration is of the policy, and display_policy_address’s own index is what later asks for one address of it.
Checksummed, which is what HWI’s parser requires of anything it is given, –desc included.
No HWI release through 3.2.0 has registerdescriptor at all, so this needs a build of master; the module docstring’s Wallet policies says what ends that.
- sign_message(message: bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview) str[source]¶
Return the compact signature of a message: HWI’s signmessage.
Octets as everywhere else in btclib, so a str is the hex of the message and bytes are the message: ecc.bms reads it that way and the two have to agree, a signature being verified here against what was signed there.
What goes on the command line is text, HWI’s signmessage taking a string and passing it to its own signer as one. The bytes are decoded as utf-8 for that, and a message that is not utf-8 is one this backend cannot be asked for – which is a limit of the command line rather than of the device, and is said as such.
- sign_psbt(psbt: Psbt) Psbt[source]¶
Return what hwi signtx answered, parsed and otherwise untouched.
Untouched deliberately: what the answer contains is checked against the psbt that was sent by psbt_signer.request_signatures, which is the caller of this and the one place that comparison belongs.
What is checked here is the other thing, and only this layer can: signtx answers signed beside the psbt – HWI computes it as “the base64 I return is not the base64 I was given” – so the flag and the two strings have to agree. A device claiming it signed while handing back what it was sent, or denying it while handing back something else, has answered inconsistently, and the psbt is not the place that shows it: the comparison is over the very strings that crossed the boundary.
A device that signed nothing is not an error and does not raise. One signer of an m-of-n answers for its own key and for no other, which is the same answer psbt.sign gives by adding nothing; what a caller compares is the psbt it gets back.
- btclib_wallet.hwi.enumerate_devices(*, executable: str | Sequence[str] = 'hwi', network: str = 'mainnet', timeout: float = 120.0, max_output: int = 1048576, emulators: bool = False) list[HwiDevice][source]¶
Return the devices HWI can see, the ones it cannot talk to included.
emulators is HWI’s own –emulators, off by default: an emulator is a device with no secure element and no owner, and enumerating one without being asked would make a test fixture look like a signer.
- btclib_wallet.hwi.is_available(executable: str | Sequence[str] = 'hwi') bool[source]¶
Whether the command line this module runs is there to be run.
Asked before running it, rather than read off a failure afterwards. A caller that offers signers of several kinds decides which to offer at all – what devices to enumerate, whether to fall back to a software signer, what to put in front of a user – and that decision comes before there is a device to ask about, so a refusal is not the shape of the answer: enumerate_devices on a host with no HWI raises SignerNotFoundError, which is the right answer to a question that was asked and the wrong way to find out that there was nothing to ask.
What is looked for is argv[0] of what would be run, which is why this belongs here and not in the caller: an executable is a name on the PATH or a whole argv – [“python”, “-m”, “hwilib”], a wrapper with flags of its own – and which part of it has to be on the PATH is this module’s own convention, _executable’s. A caller writing shutil.which(“hwi”) beside it writes the default name a second time and takes that convention as read.
True is not a promise that a device will answer, or that what is on the PATH is HWI at all: it is that there is something to run, which is the half of it a caller cannot find out without running one.
btclib_wallet.minikey module¶
Casascius minikey support.
A minikey is a short base58 string – 20 characters or more, always starting with S – that stands for a private key: sha256(text) is the key itself, and sha256(text + “?”) beginning with a zero byte is the format’s own typo check. Casascius physical bitcoins printed one on the paper disc under their security hologram, at 22 characters for the Series 1 coins and 30 for every series after – neither length is enforced on its own here, matching electrum/bitcoin.py:757-769 (spesmilo/electrum, pinned at 342bca3a) exactly.
The string is never base58-decoded. A minikey carries no version prefix and no Base58Check payload behind it – the base58 alphabet is only the character set it happens to be drawn from – so this module owns the format rather than btclib.b58, which decodes Base58Check and nothing else.
The format is dead, and read-only by design. Casascius stopped making physical coins in 2013, after FinCEN classified them as money transmission; a 30-character minikey carries about 160 bits of entropy and a 22-character one about 128, well short of what a private key generated today should have. btclib reads a minikey because the coins already exist and their keys have to be swept out of them, and it publishes no way to build one.
- btclib_wallet.minikey.prv_key_data_from_minikey(minikey: str) PrvKeyData[source]¶
Return the private key a Casascius minikey encodes.
Two error classes, in refusal order, the same split b58.prv_key_data_from_wif draws for a WIF. The first three checks below – the length, the leading S, the alphabet – ask “is this shape a minikey at all”, and a no is a NotAPrvKeyError: nothing else in this format could have produced such a string, so a caller trying several key formats in turn is told to keep going rather than stop. The ?-suffix check asks “is this particular, well-shaped minikey sound”, and a no is an InvalidPrvKeyError: the shape is a minikey’s, the typo check is what failed, and it is what the check exists to catch – with probability 255/256 for one wrong character, never a proof that no character is wrong, and no other format will read the same string differently.
No message echoes the input or any part of it: unlike a WIF, whose text is Base58Check encoding a payload distinct from it, a minikey’s text is the key material itself, sha256(minikey) being the private key it stands for.
The key a minikey encodes carries no network of its own – Casascius coins are a mainnet artefact – and, by the same physical-coin convention, no compression flag beyond “uncompressed”: both are fixed rather than read from the string.
btclib_wallet.psbt_signer module¶
The contract an external signer answers, and the checks on its answers.
A hardware wallet, a signing service, another process: something that holds keys btclib does not have and answers questions about them. The protocols here are what it implements; the functions beside them are what a caller should run over its answers, and they are the point of the module – a protocol alone is an interface, and every one of these answers arrives from outside and can be wrong.
This is not psbt.sign, which is the Signer role played over a KeyManager btclib calls in-process: that one derives keys and signs, and its answers are btclib’s own. Here the psbt goes out and comes back untrusted, so the two need different trust models and are two contracts.
What each function checks, which is what the caller would otherwise have to remember:
request_signatures holds the returned psbt to the one that was sent – psbt.assert_signatures_only, so nothing but signatures came back – and only then combines the two;
export_account builds the descriptors of an account from the fingerprint and the xpub a signer answers with, and descriptors.account_descriptors is what refuses an xpub that is not the account the path names;
display_address compares the address a device shows with the one the descriptor describes, which is the whole point of asking a device to show one;
display_policy_address is the same check for a device-registered BIP388 wallet policy: descriptors.wallet_policy_address computes the address the policy describes at an index and a multipath index, and what is compared is what display_address compares for a plain descriptor;
sign_message verifies the signature against the address the caller says it must open to.
Nothing here sends a private key anywhere, and nothing can: a Descriptor holds no key that signs, descriptors.parse having neutered what it read, and assert_public is what says so of one built by hand rather than parsed. A psbt has no field for a private key at all.
Selecting which device answers, the transport it answers over, and the timeouts and output limits a subprocess needs are the adapter’s, not this module’s: this is the contract such an adapter implements (issue btclib-org/btclib#381).
SignerDecorator is the other thing a caller writes against the contract: a signer wrapping a signer, for the rules that are the caller’s own – a whitelist of outputs, a limit on what may be spent, a prompt somebody has to confirm. The rule belongs nowhere near this library and the forwarding does, being what goes wrong when it is written by hand.
- class btclib_wallet.psbt_signer.AddressDisplay(*args, **kwargs)[source]¶
Bases:
ProtocolA signer that can show an address on a screen of its own.
Optional, and separate from PsbtSigner for the reason the issue behind this module gives: a signer that cannot show anything is still a signer, and a caller asks with isinstance rather than being told.
- display_address(descriptor: Descriptor, index: int = 0) str[source]¶
Return the address the signer shows for a descriptor at an index.
- class btclib_wallet.psbt_signer.MessageSigner(*args, **kwargs)[source]¶
Bases:
ProtocolA signer that can sign a message with a key it holds.
Optional in the same way, and the message is not a transaction: what comes back is a BIP137 compact signature, which sign_message checks against the address the caller says it must open to.
- class btclib_wallet.psbt_signer.PsbtSigner(*args, **kwargs)[source]¶
Bases:
ProtocolWhat every external signer answers: keys, a signature, an end.
The three questions a caller cannot answer for itself and one piece of housekeeping. Nothing here is about a device in particular – a subprocess adapter, a signing service and a software signer implement the same four – which is what makes it the boundary rather than a driver.
- property capabilities: SignerCapabilities¶
Return what this signer can be asked to sign.
A property for master_fingerprint’s reason: what every implementation answers is a value it was given or built once.
- close() None[source]¶
Release whatever the signer holds: a handle, a process, a socket.
Idempotent, so that a caller may close a signer it is not sure about; contextlib.closing is the customary way to run one.
- property master_fingerprint: bytes¶
Return the four bytes identifying the master key, BIP32’s own.
A property, and that is a promise the contract makes: reading it is free. Every implementation there is keeps it – HwiSigner holds the fingerprint it was selected by and says so (“not asked of the device again”), SoftwareSigner derives it from a key it already has, and a decorator forwards – so nothing is asked to pay for the shape (issue btclib-org/btclib#814).
It is worth knowing what would ask for the shape back. HWI’s own HardwareWalletClient.get_master_fingerprint is a device call: it fetches the key at m/0h and reads the parent fingerprint off it. An adapter written against that library rather than against the command line would want a method here, and the way to give it one is to relax this to a method again – which is a change to the contract, made deliberately, and not something to leave room for in advance.
- class btclib_wallet.psbt_signer.SignerCapabilities(taproot: bool = False, musig2: bool = False)[source]¶
Bases:
objectWhat a signer can be asked to sign, in the terms btclib acts on.
Two flags and not a device matrix: which models support what is a table HWI maintains per vendor and per firmware version, and a library that copied it would be wrong the week after. What belongs in a contract is what a caller does differently on the answer, and there are two such facts – a taproot input needs a signer that knows BIP341, and a MuSig2 session needs one that knows BIP327 and BIP373.
Which operations a signer offers is not here either: that is what the optional protocols below say, and a caller asks with isinstance, both being runtime-checkable. A flag saying “I can display an address” beside a display_address method would be the same fact twice, and the two would disagree.
- class btclib_wallet.psbt_signer.SignerDecorator(signer: PsbtSigner)[source]¶
Bases:
objectA signer that wraps a signer, for a caller adding a rule to one.
The shape every “sign, but only if” is: a rule of the caller’s own in front of sign_psbt, and everything else answered by the signer underneath. A whitelist of outputs a device may pay to, a limit on what a psbt may spend, a log of every request, a prompt somebody has to confirm – what those have in common is not the rule, which is the caller’s business and belongs nowhere near this library, but the four other methods, which have to keep answering exactly what the wrapped signer answers. A subclass overrides the one it is about:
class Whitelisted(SignerDecorator): def sign_psbt(self, psbt: Psbt) -> Psbt: for out in psbt.tx.vout: ... # the caller's rule, before the device return super().sign_psbt(psbt)
Written out here because forwarding is what goes wrong when it is written by hand: a wrapper that answers capabilities for itself tells a caller a taproot input cannot be signed by a signer that can, and one that forgets close leaves a subprocess running after the caller closed what it was holding.
Wrapping does not hide what the signer offers. AddressDisplay, WalletPolicyAddressDisplay and MessageSigner are optional protocols a caller asks about with isinstance, so a wrapper that never carries them turns a device that can show an address into one that cannot, and a wrapper that always declares them turns a signer that cannot into one that fails when asked. Each operation is therefore bound on the instance, and only where the wrapped signer has it, so isinstance answers what the signer offers – and a subclass that writes one of its own keeps it, an attribute written by a class being what says the subclass means to answer that question itself.
On the instance rather than through __getattr__, which is the way this is usually written and is wrong in a way nothing reports: since 3.12 a runtime-checkable protocol is checked with inspect.getattr_static, which does not call __getattr__, so a wrapper delegating that way satisfies isinstance on 3.11 and stops satisfying it on 3.12 and after – the same wrapper, the same signer, a different answer per interpreter.
Nothing else is forwarded. What this is is the contract, not the surface of the adapter underneath: a caller that wants an attribute of the signer it wrapped reads .signer, which is what it passed in.
- property capabilities: SignerCapabilities¶
Return what the wrapped signer can be asked to sign.
- class btclib_wallet.psbt_signer.SignerDevice(*args, **kwargs)[source]¶
Bases:
ProtocolWhat a caller knows of a device before it has a signer for it.
Selecting which device answers is the one thing a caller does with no signer in hand, and it is the same rule for every transport: the fingerprint identifies the master key, so it identifies the device that holds it, whatever answered for it. hwi.HwiDevice satisfies this; so does a caller’s own record of a signer that is not a device at all.
fingerprint is None for a device that cannot be asked for one yet – a locked Trezor, a Ledger with no app open – and error says why. Such a device is listed on purpose: what a caller does about a locked device is unlock it, and a list that left it out would say it is not there.
- class btclib_wallet.psbt_signer.SoftwareSigner(xkey: BIP32KeyData | bytes | str | bytearray | memoryview, *, musig2: bool = False)[source]¶
Bases:
objectKeys in this process, at known paths, answering the contract above.
The reference implementation of PsbtSigner, AddressDisplay and MessageSigner, and what makes the contract testable without hardware: a deterministic signer is a signer whose answers a test can predict, so every psbt shape the library builds can be signed end to end here before any device is involved.
It is not a way to sign with a key you hold – psbt.sign over a KeyManager is that, and this calls it. What this adds is the boundary: it answers only what the protocol asks, by deriving what it is told to derive, and it holds nothing about the caller. Which is why it is worth having beside a device rather than instead of one – an adapter and this answer the same questions, so a caller can be developed against this and run against that.
A key is answered for when the origin’s fingerprint is this signer’s and the path derives to the very public key the psbt names. That second half is the check a device makes too: a psbt saying “this key is at that path” is a psbt somebody else wrote, and signing with what the path derives to without looking would sign with a key the caller was not told about.
One key or several is the same model: what is held is a key at a path from the master, and SoftwareSigner(xkey) is the case where that path is empty and the key is the master. from_accounts is the other case, where a device exported accounts and kept its master – the shape a psbt names, its key origins being a master fingerprint and a path from it.
- property capabilities: SignerCapabilities¶
Return what this signer can be asked to sign.
Taproot always: psbt.sign signs the key path and every leaf the psbt names this signer’s keys in, which is both halves of a BIP341 output for a signer holding what they ask for. MuSig2 is a constructor argument and defaults to False, because the rounds of BIP373 are psbt.musig2’s and are played by a caller holding the secret nonce between them – this signer signs in one call and cannot answer for a session it does not hold.
- close() None[source]¶
Mark the signer closed; there is nothing to release.
A software signer holds no handle and no process, so this exists for the contract: a caller that closes every signer it opens is a caller that works with a device too. Asking a closed signer anything raises, which is what makes the difference visible in a test rather than only against hardware.
Nothing survives a signature here, which is what keeps that true: sign_schnorr_script_path below owns its ssa.Signer for the length of a call and wipes it on the way out, so there is no keypair whose release rests on a caller remembering this method.
- display_address(descriptor: Descriptor, index: int = 0) str[source]¶
Return the address the descriptor describes, as a screen would.
There is no screen here, so what this answers is what a device would show if it agreed – which makes display_address above a check of the descriptor against itself when this signer is the one asked. That is what a reference implementation is for: the caller’s flow runs unchanged, and the check that matters is the one made against a device that could have disagreed.
- classmethod from_accounts(master_fingerprint: Octets, accounts: Mapping[DerPath, BIP32Key], *, musig2: bool = False) SoftwareSigner[source]¶
Return a signer holding accounts, for the master they came from.
What a device that exported its accounts leaves behind, and what SoftwareSigner(xkey) cannot express: the fingerprint a psbt names is the master’s, and an account key’s own is a different four bytes, so a signer built on an account would answer for no origin the device’s psbts carry.
The fingerprint is therefore told rather than computed, and is a claim: nothing in an extended key records where it came from, so an account paired with the wrong master answers for origins whose keys it does not hold – and the public key check refuses each of them, one derivation later.
Each path is the account’s own, from that master. An origin is answered by the account whose path is a prefix of it, the remainder being what is derived; where two accounts prefix the same origin the longer one answers, having less left to derive. Fingerprint, then prefix, then the public key: the three are what HWI’s ledger driver matches on and what electrum’s keystore tries first, which is the shape a psbt written by a wallet has.
An origin naming an account’s own fingerprint rather than the master’s is the other thing a wallet writes, and it is SoftwareSigner(account_xkey): a signer answers one fingerprint, master_fingerprint being a single question, so which of the two a psbt carries decides which constructor reads it.
What is not done is electrum’s third attempt, which ignores the fingerprint and tries the last few indexes against the public key anyway. It is a search for a key the psbt did not say is there, and a match found that way is a coincidence a signature would make binding.
- property master_fingerprint: bytes¶
Return the fingerprint of the key this signer was built on.
The master fingerprint of the contract, which is this key’s own and is therefore a claim only as true as the key handed in: a signer built on an account xpub answers that account’s fingerprint, and a key origin naming it would send another signer looking for a master key nobody has.
- sign_ecdsa(pub_key: bytes, origin: BIP32KeyOrigin | None, msg_hash: bytes) bytes | None[source]¶
Return the DER signature of msg_hash by pub_key, or None.
- sign_message(message: bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview) str[source]¶
Return the BIP137 compact signature of a message, base64.
The address the signature opens to is the p2pkh one of the key at the path, which is what ecc.bms signs with by default and what a caller checks it against.
- sign_psbt(psbt: Psbt) Psbt[source]¶
Return the psbt with a signature for every key this one holds.
psbt.sign over a KeyManager this class implements, which is the whole of it: the roles are btclib’s already, and a reference signer that re-derived the sig_hash itself would be a second implementation to keep right.
A watch-only signer raises rather than answering the psbt unchanged: “I hold none of these keys” and “I hold no key at all” are different answers, and only the first is a psbt somebody else can carry on with.
- sign_schnorr(pub_key: bytes, origin: BIP32KeyOrigin | None, msg_hash: bytes, merkle_root: bytes) bytes | None[source]¶
Return the BIP340 signature of msg_hash by pub_key, or None.
Tweaked by the merkle root before signing, which is the KeyManager contract: the signature has to be the output key’s, and sign never holds what tweaking a private key needs.
- sign_schnorr_script_path(pub_key: bytes, origin: BIP32KeyOrigin | None, msg_hash: bytes, leaf_hash: bytes) bytes | None[source]¶
Return the BIP342 signature of msg_hash by pub_key, or None.
Untweaked, which is the KeyManager contract for a script path: what the spend proves is the leaf, and the output key’s tweak is proved by the control block instead.
Every leaf the psbt names this key in is signed for. A device would show the leaf script and ask, and leaf_hash is what it would find it by; a signer holding keys at known paths and answering in one call has no user to ask, so what it answers for is decided by the same three conditions as the other two methods.
ssa.Signer and not ssa.sign_, for one leaf as for many: what it saves is the Sig that sign_ builds and serialize takes apart again, and a psbt wants the octets. The keypair it holds is built and wiped inside the call.
Holding one across calls was measured and is not done. Those leaves are the one place this library signs BIP340 more than once under one key, and a keypair kept between them is cheaper per leaf – but only from the second leaf of a key onward, and a key in a single leaf is the ordinary shape. The saving is the smaller half of what Signer buys and the only half that makes a secret outlive the call that needed it, which is not a trade to make for a case that may not arise.
- property xkey: str¶
Return the key this signer was built on, where it was built on one.
A signer holding accounts was built on none: there is no key of which the others are derivations, and answering one of them would be answering a key the caller did not ask about.
- xpub(der_path: str | Sequence[int] | int | bytes | bytearray | memoryview) str[source]¶
Return the extended public key at a path, neutered whatever it is.
Public whether this signer holds a private key or not: what the contract asks for is an xpub, and answering an xprv would put a key that signs where a caller expects one that cannot.
- class btclib_wallet.psbt_signer.WalletPolicyAddressDisplay(*args, **kwargs)[source]¶
Bases:
ProtocolA signer that can show the address of a policy it has registered.
Optional and separate from AddressDisplay for the same reason that one is separate from PsbtSigner: a signer with no screen for a registered BIP388 policy is still a signer. What a policy needs that a plain Descriptor does not is a third parameter neither AddressDisplay’s signature nor its caller has room for – the multipath index BIP389’s <M;N> needs beside the ordinary one – so this is a second protocol rather than a widened first one.
registration is what a signer answered when the policy was registered; registering it is not part of this contract, the same way showing an address is not part of PsbtSigner – HwiSigner .register_descriptor is where that happens, and the module docstring of btclib_wallet.hwi, “Wallet policies”, says why it has none of its own.
- btclib_wallet.psbt_signer.assert_public(descriptor: Descriptor) None[source]¶
Raise if any key of the descriptor is one that signs.
Nothing descriptors.parse returns can fail this: it neuters every xprv it reads and hands the private spelling back to its caller. What this catches is a descriptor built by hand, which the fragment classes are public enough to allow – and the moment before it is sent to something outside the process is the moment to catch it.
- btclib_wallet.psbt_signer.display_address(signer: AddressDisplay, descriptor: Descriptor, index: int = 0) str[source]¶
Return the address the signer shows, having checked it is the right one.
The whole point of asking a device to display an address is that the screen is the one part of it a compromised host cannot rewrite – so what the device says has to be compared with what the descriptor describes, and a caller that shows the user its own answer instead has checked nothing.
The descriptor is checked to hold no key that signs before it is sent anywhere, which is assert_public.
- btclib_wallet.psbt_signer.display_policy_address(signer: WalletPolicyAddressDisplay, registration: str, template: str, key_info: tuple[KeyExpression, ...], index: int = 0, multipath_index: int = 0, network: str = 'mainnet') str[source]¶
Return the address the signer shows for a registered policy, checked.
The same check display_address runs for a plain Descriptor, over a BIP388 wallet policy instead: template and key_info are the pair descriptors.wallet_policy returns, and descriptors.wallet_policy_address is what computes the address they describe at index and multipath_index – the second index BIP389’s <M;N> needs beside the ordinary one.
key_info is a tuple and not the Sequence descriptors.wallet_policy_address itself takes: a list built by some other route needs tuple(…) first. That is narrower than the computation asks for, and is not a design choice – a Sequence argument here reads ambiguously in the documentation build, this class being importable both as btclib_wallet.descriptors.KeyExpression and as btclib_wallet.descriptors.key_expression.KeyExpression, and a bare parameter resolves that where a subscripted one does not. tuple sidesteps it and costs nothing for the caller wallet_policy itself feeds, which is the ordinary one.
registration is not checked here: it is register_descriptor’s own opaque answer, sent back unexamined the way HwiSigner .register_descriptor’s docstring says a caller must.
- btclib_wallet.psbt_signer.export_account(signer: PsbtSigner, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview, script_type: Literal['p2pkh', 'p2wpkh-p2sh', 'p2wpkh', 'p2tr'] | None = None) tuple[Descriptor, Descriptor][source]¶
Return the receive and change descriptors of an account of a signer.
Two questions to the signer and one composition: descriptors.account_descriptors builds the pair from the master fingerprint and the xpub at the account path, and is what refuses an xpub that is not the account the path names – its depth and its own index say which account it is, and a purpose the mapping does not know is refused rather than guessed.
What cannot be checked here is that the xpub descends from that fingerprint at all: an extended key records nothing about where it came from, and a signer that answered with another key would need a second, independent path to be caught – an address the device shows for the same descriptor, which is display_address.
- btclib_wallet.psbt_signer.merge_devices(*sources: Sequence[SignerDevice]) list[SignerDevice][source]¶
Return one list of devices from several, the earlier source winning.
A caller with more than one way of reaching a signer – a command line, an in-process driver, keys of its own – has one question to answer that none of the adapters can: which of them answers for a fingerprint two of them offer. Order is that answer, and it is the caller’s to state, so the sources are positional and the first one that names a fingerprint keeps it.
Devices that cannot be asked for a fingerprint yet are all kept: there is no fingerprint to be a duplicate of, and dropping them would say a locked device is not plugged in.
- btclib_wallet.psbt_signer.request_signatures(signer: PsbtSigner, psbt: Psbt) Psbt[source]¶
Return the psbt with what the signer added, checked and merged.
Three steps and the middle one is why this exists: the psbt goes out, the answer is held to it – everything that is not a signature comes back as it was sent, the signature fields may only have gained entries, and every signature that arrived verifies – and only then are the two combined.
Skipping that check is not a smaller version of this call: combine takes the union of what it is given and resolves a conflict by picking a side, so an answer that changed an amount, an outpoint or somebody else’s signature would be merged in without a word.
The psbt handed in is left alone, combine returning a copy of its own, so a caller can ask several signers with the same request and combine the answers itself.
- btclib_wallet.psbt_signer.select_device(devices: Sequence[SignerDevice], fingerprint: Octets) SignerDevice[source]¶
Return the one device answering for a fingerprint, or raise.
Two failures and they are different news, so they are different messages: no device answers for it, or one does and it said why it cannot be asked – which is a device to unlock rather than a device to look for.
More than one is not among them: merge_devices is where a caller states which source wins, and a single source answering one fingerprint twice is two cables to one device, so the first is taken.
- btclib_wallet.psbt_signer.sign_message(signer: MessageSigner, message: bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview, address: bytes | str | bytearray | memoryview) str[source]¶
Return the signature of a message, verified against an address.
The address is a parameter and not something this works out: a BIP137 signature carries a recovery flag that says which address type it is for, and which address a caller means is a fact about the key it asked for rather than about the signature that came back. What is checked is the one thing that matters – the signature opens to that address – and ecc.bms.assert_as_valid is the check.
btclib_wallet.psbt_signer_contract module¶
Check an implementation of psbt_signer’s contract, from outside it.
psbt_signer says what a signer answers; this says whether one does. A protocol is a promise the type checker reads and nothing runs, so an adapter that returns a five-byte fingerprint, or a psbt it edited, or an xpub it derived from the wrong path, type-checks and is wrong at the first spend.
btclib’s own adapters are checked by btclib’s tests, which is no help to the caller writing the next one: an implementer outside this repository had no way to ask the library whether their signer answers what callers of it will assume. assert_psbt_signer is that question, and it takes any PsbtSigner – a command line adapter, an in-process driver, a signing service, a signer that is not a device at all.
It is a function and not a test suite so that it belongs to no test framework: call it from pytest, from unittest, from a script run against the hardware on a bench. It raises on the first breach, with what was expected and what came back, because the first breach is the one to fix and a list of consequences of it is not more information.
What it checks is what a type cannot say. A method returning the wrong type is what the implementer’s own type checker reports, and repeating that here would be a second, weaker copy of it; a fingerprint of the wrong width, an xpub that is not one, a key answered privately, a signature added to somebody else’s input – none of those are type errors, and all of them type-check.
What it does not check is whether the signatures are right. That is request_signatures, which holds an answer to the psbt that was sent and verifies every signature that arrived – the check that matters most is the one a caller runs on every spend, not one a conformance pass runs once. What this adds is the shape of the answers around it: the things request_signatures assumes and does not restate.
Two of the checks need material only the caller has, and both are optional. A signer holds keys at paths this module cannot guess, so der_path is asked for rather than defaulted – a wrong guess would report a conforming signer as broken. And a psbt the signer can actually sign is the only way to see it sign, so signable is what turns a shape check into an end-to-end one.
- class btclib_wallet.psbt_signer_contract.OptionalProtocols(displays_address: bool, displays_policy_address: bool, signs_message: bool)[source]¶
Bases:
NamedTupleAnswers to unrelated questions, which is a record and no sequence.
A plain tuple is right for a sequence – things of one kind whose count is the point, where an index is a position and means it. displays_address, displays_policy_address and signs_message answer questions that have nothing to do with each other, and how many of them there are is an accident of which optional protocols exist today. An index into a record is a position standing in for a name, and the name is what a caller reading [1] meant.
The field names are an escape from that for a caller who reads them, not a guard: a caller who indexes rather than names a field reads the wrong answer the moment a protocol is inserted rather than appended.
- btclib_wallet.psbt_signer_contract.assert_psbt_signer(signer: object, *, der_path: DerPath | None = None, signable: object = None) None[source]¶
Check a signer against the contract, raising at the first breach.
der_path is a path the signer holds a key at; without one the xpub checks are skipped, since a path guessed here would report a conforming signer as broken. signable is a psbt the signer can sign; without one the checks are of shape alone, and nothing sees it sign.
Both it and the signer are typed object rather than what they have to be. A function whose subject is what a type cannot promise is one that has to be callable with what a type would have refused, and it says what arrived instead of the caller’s type checker saying it first: an adapter written without one is exactly the caller this exists for.
Closing is checked last and twice, close being documented as idempotent – a caller closes a signer it is not sure about – so the signer is spent when this returns.
- btclib_wallet.psbt_signer_contract.optional_protocols(signer: object) OptionalProtocols[source]¶
Return which optional protocols the signer offers, as a caller asks.
isinstance against the three runtime-checkable protocols, which is the whole of it: what display_address, display_policy_address and sign_message answer is checked by the functions of psbt_signer that call them, against the descriptor, the policy or the address the caller says the answer must match, and a conformance pass has none of that material.
- btclib_wallet.psbt_signer_contract.unsignable_psbt(fingerprint: bytes) Psbt[source]¶
Return a psbt of one input no signer of this fingerprint can sign.
A p2wpkh whose key origin names somebody else’s master, which is the shape of the psbt a caller sends to every signer it has and expects most of them to hand straight back. Built from the signer’s own fingerprint with a bit flipped, so it is somebody else’s for this signer whoever it is, and no caller has to supply a key to find out what the signer does with a psbt that is not its business.
btclib_wallet.silent_payments module¶
Silent payments, according to BIP352.
https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
A silent payment address is published once and reused; every payment to it lands on a different taproot output, so nothing on chain links two payments to the same recipient. The recipient publishes two keys, a scan key and a spend key, and the sender derives the output from an ECDH shared secret between its own input keys and the scan key – so the output is one the sender can compute and only the recipient can spend, with no interaction and nothing extra in the transaction.
Where the shared secret comes from is the whole design. It is not the sender’s ephemeral key, which would need a place in the transaction to publish it; it is the sum of the private keys the sender is signing the inputs with. The recipient recovers the same secret from the public keys of those inputs, which the transaction already carries, so scanning is one multiplication per transaction and the transaction is an ordinary taproot spend.
The pieces, bottom-up:
pub_key_from_input reads the public key of one input, and answers None for an input BIP352 does not count. Only p2pkh, p2wpkh, p2sh-p2wpkh and p2tr count: an input with conditional branches or several keys could be re-signed with a different set after the output was derived, which in a coinjoin is somebody else’s malleability, and uncompressed keys are excluded as BIP143 already recommends.
prv_key_sum and pub_key_sum are the two sides of the same sum, the taproot negation included: an x-only key has two private keys, and sender and recipient have to pick the same one.
input_hash binds the sum to the transaction’s smallest outpoint, so that the same input keys spent in two transactions derive two different outputs. tweak_data is that hash times the public sum, which is what a light-client server can publish per transaction (BIP352’s Appendix A) and all a scanner needs.
shared_secret is the multiplication both parties do, from either end.
output_keys is the sender’s whole operation, and scan_outputs the recipient’s – for a caller holding a light client’s tweak, per BIP352’s Appendix A; scan_transaction_outputs is the same recipient’s operation for a caller holding the transaction itself, outpoints and input public keys, which is what lets it reach the bindings the way output_keys does.
label_tweak, labeled_address_from_keys and label_lookup are the optional third piece: one published address per purpose, all sharing one scan key, at the cost of a subtraction per output while scanning.
What is not here is the transaction-level policy, which is a wallet’s: BIP352 says a transaction is worth scanning when it has a taproot output, has an eligible input, and spends no output of segwit version above 1, and that a sender must sign with a sighash flag that fixes the inputs – SIGHASH_ANYONECANPAY breaks the protocol, the inputs being what the secret is derived from. None of the three is a function here; the module docstring is where they are stated, and btclib.script.sig_hash is where the flags are.
secp256k1 and sha256 are not parameters, as in btclib.ecc.musig2: BIP352 is defined for that pair, and the 33-byte compressed points, the 32-byte scalars and the three tags below are its serialization.
- class btclib_wallet.silent_payments.SilentPaymentOutput(pub_key: bytes, prv_key_tweak: int)[source]¶
Bases:
objectA silent payment output a scan found, and what it takes to spend it.
pub_key is the 32-byte x-only taproot output key, which is what the transaction carries and what identifies the output
prv_key_tweak is the scalar to add to the spend private key, t_k plus the label tweak where a label was used: prv_key_from_tweak does that addition
- btclib_wallet.silent_payments.address_from_keys(B_scan: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, B_m: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, network: str = 'mainnet') str[source]¶
Return the bech32m silent payment address of a key pair.
B_m is the spend key, or the spend key plus a label tweak: labeled_address_from_keys is the spelling that applies the tweak.
- btclib_wallet.silent_payments.input_hash(outpoints: Sequence[OutPoint], A_sum: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) int[source]¶
Return the scalar binding the input keys to this transaction.
The smallest outpoint lexicographically, hashed with the public sum: without it the same input keys spent in two transactions would derive the same outputs, and a sender could be made to pay twice to one address. Lexicographically on the 36 wire bytes, which are little-endian – so the ordering is the transaction’s own and a wallet parsing a serialized transaction reorders nothing.
An empty sequence has no smallest outpoint and no input hash.
- btclib_wallet.silent_payments.keys_from_address(address: bytes | str | bytearray | memoryview) tuple[tuple[int, int], tuple[int, int], Literal['main', 'test']][source]¶
Return (B_scan, B_m, network type) from a silent payment address.
The network type and not a network: BIP352 has one hrp for mainnet and one for every test network, so “tsp” says testnet, signet, testnet4 or regtest without saying which.
A version above 0 is read as far as v0 defines it – the first 66 bytes of the payload, the rest discarded – so that a v0 sender can pay a later address. v31 is refused instead, being the version BIP352 reserves for a change that breaks exactly that.
- btclib_wallet.silent_payments.label_lookup(b_scan: bytes | str | bytearray | memoryview | int, m_values: Iterable[int]) dict[bytes, bytes][source]¶
Precompute the {label point: label tweak} map a scan reads.
Once per wallet, not once per transaction, which is the point of it: scanning subtracts the candidate output from P_k and asks whether the difference is a label, so a wallet with M labels pays M multiplications here instead of M point additions per output for ever. Include 0 among the values unless the wallet is certain it never paid itself.
The tweak is 32 bytes, big-endian – silentpayments.scan_outputs’s own spelling of a label cache, Mapping[bytes, bytes], which refuses a bytearray or a memoryview as a key and is what scan_transaction_outputs hands the bindings unconverted where they serve. scan_outputs’s Python loop reads the same bytes through scalar_from_prv_key, which already accepts 32-octet SEC input, so neither arm pays a conversion this function did not already do once, per wallet rather than per scan.
- btclib_wallet.silent_payments.label_tweak(b_scan: bytes | str | bytearray | memoryview | int, m: int) int[source]¶
Return the scalar labelling an address with the integer m.
The scan private key is what the tweak is derived from, so the recipient can recognize its own labels while scanning without holding the spend key: BIP352 exports the scan key on purpose, and a label derived from the spend key would have undone that.
- btclib_wallet.silent_payments.labeled_address_from_keys(b_scan: bytes | str | bytearray | memoryview | int, B_spend: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, m: int, network: str = 'mainnet') str[source]¶
Return the address of the spend key labelled with the integer m.
m = 0 is the change label, reserved by convention for the outputs a sending wallet pays to itself; BIP352 asks a scanner to check it always, which is what makes that convention safe to rely on when recovering a wallet from a seed alone.
- btclib_wallet.silent_payments.output_key(secret: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, B_m: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, k: int) bytes[source]¶
Return the x-only taproot output key of one recipient of a group.
The last step of BIP352’s derivation, and the one a caller that already holds the shared secret needs on its own: btclib_wallet.psbt.silent_payments reaches this point from an ECDH share a psbt carries rather than from a private key, so what the two paths share is this and not output_keys.
k is the recipient’s position in its group, which is what stops two payments to one scan key landing on one output.
- btclib_wallet.silent_payments.output_keys(prv_keys: Sequence[tuple[bytes | str | bytearray | memoryview | int, bytes | str | bytearray | memoryview]], outpoints: Sequence[OutPoint], addresses: Sequence[bytes | str | bytearray | memoryview]) list[bytes][source]¶
Return the x-only taproot output keys to pay a list of addresses.
One key per address, in the order the addresses are given; btclib.script.script_pub_key.ScriptPubKey.p2tr turns each into the output to put in the transaction. Repeat an address to pay it twice: the k that separates two outputs of one recipient is its position in that recipient’s group, so two payments to one address are two different outputs.
prv_keys pairs each input’s private key with the script_pub_key it spends, as prv_key_sum takes them, and every eligible input of the transaction must be there – the recipient sums all of them. The outpoints are the transaction’s, eligible or not: what the input hash binds to is the transaction.
Every key returned must be in the final transaction. The k of a group is what a scanner increments, and it stops at the first k it does not find: dropping the i-th output of a group hides every later one from its recipient.
Grouping is by scan key, so two labelled addresses of one recipient share a group and get consecutive k. That is deliberate: reusing one t_k for both would make the difference of the two output keys equal the difference of the two published addresses, which is the recipient named in public.
Where the bindings serve secp256k1 – BIP352 has no other curve to ask them for – this is silentpayments.create_outputs’s own derivation rather than the Python arithmetic below: one keypair build per taproot input and one shared-secret multiplication per recipient group inside libsecp256k1, in place of mult and mult_pub_key here. a and h are computed either way, for the refusal a zero private-key sum or an empty outpoint sequence already has a specific message for – see _delegated_output_keys.
- btclib_wallet.silent_payments.prv_key_from_tweak(b_spend: bytes | str | bytearray | memoryview | int, prv_key_tweak: int) int[source]¶
Return the private key that spends a found output.
b_spend plus the tweak scan_outputs reported, modulo n. The taproot output is x-only, so a signer negates this key if it has to; that is BIP340’s business and btclib.ecc.ssa.sign does it.
- btclib_wallet.silent_payments.prv_key_sum(prv_keys: Sequence[tuple[bytes | str | bytearray | memoryview | int, bytes | str | bytearray | memoryview]]) int[source]¶
Return the sum of the input private keys, taproot ones negated.
Each pair is one input’s private key and the script_pub_key it spends. The script and not a flag beside it: whether to negate is is_p2tr of that script, and a caller keeping a boolean in step with it is a caller with one more thing to get wrong.
The negation is BIP340’s two private keys per x-only key, d and n-d: the recipient sums the x-only public keys and so assumes the even-y one, and a sender that summed the other derives an output nobody finds.
A sum of zero is BIP352’s “fail”, and it is not the same as an empty sequence: Input keys sum up to zero is a real vector – two inputs whose keys are negatives – and the payment cannot be made, the shared secret being the point at infinity. An intermediate zero is fine and is a vector too.
- btclib_wallet.silent_payments.pub_key_from_input(script_pub_key: bytes | str | bytearray | memoryview, script_sig: bytes | str | bytearray | memoryview = b'', witness: Witness | None = None) tuple[int, int] | None[source]¶
Return the public key of one input, or None if it does not count.
None is BIP352’s “skip”: the four eligible output types are p2pkh, p2wpkh, p2sh-p2wpkh and p2tr, and inside them an uncompressed key, a taproot NUMS internal key, a p2sh wrapping anything but p2wpkh, and a scriptSig or witness that carries no key at all are each skipped rather than refused. A transaction of nothing but skipped inputs is a transaction no silent payment can be made from, which is the caller’s to notice – pub_key_sum of an empty sequence says so.
- btclib_wallet.silent_payments.pub_key_sum(pub_keys: Sequence[bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint]) tuple[int, int][source]¶
Return the sum of the input public keys, refusing infinity.
Infinity is BIP352’s “skip the transaction” on the receiving side and the failure prv_key_sum reports on the sending one; a caller scanning rather than paying reads it as the skip it is. An empty sequence is the same answer, and is what a transaction of nothing but skipped inputs sums to.
One keys.pubkey_sum of all the terms rather than a running total added one at a time: what kept it here was that an intermediate sum at infinity is a BIP352 vector and infinity is what libsecp256k1 has no public key for, and sum_var is where that stopped being a reason – a sum at infinity comes back as a value now, and this function still refuses it.
- btclib_wallet.silent_payments.scan_outputs(b_scan: bytes | str | bytearray | memoryview | int, B_spend: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, tweak: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, outputs_to_check: Sequence[bytes | str | bytearray | memoryview], labels: Mapping[bytes, bytes] | None = None) list[SilentPaymentOutput][source]¶
Return the outputs of one transaction that belong to this wallet.
tweak is the tweak data of tweak_data, input_hash*A_sum – the light client’s entry point, BIP352’s Appendix A: a server hands a light client exactly this and nothing else about the transaction, which is why this stays the Python arithmetic below whatever the bindings serve. scan_transaction_outputs is the counterpart for a caller holding the transaction itself. outputs_to_check are the x-only keys of every taproot output of the transaction, spent ones included – a wallet recovering its history is looking for outputs it has already spent. labels is label_lookup’s map, and BIP352 asks for the change label m = 0 in it whatever else the wallet used.
The scan walks k upwards and stops at the first k that matches nothing, which is what makes it one multiplication per transaction rather than one per output. That stopping rule is also why the decision to continue must be the cryptographic match and nothing else: an output found and then dropped by a wallet policy – dust, say – still has to advance k, or every later output of the same sender is missed.
- btclib_wallet.silent_payments.scan_transaction_outputs(b_scan: bytes | str | bytearray | memoryview | int, B_spend: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, outpoints: Sequence[OutPoint], pub_keys: Sequence[tuple[bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, bytes | str | bytearray | memoryview]], outputs_to_check: Sequence[bytes | str | bytearray | memoryview], labels: Mapping[bytes, bytes] | None = None) list[SilentPaymentOutput][source]¶
Return the outputs of one transaction, from data a full node has.
scan_outputs is the light client’s entry point, tweak the one value BIP352’s Appendix A hands it. A full node holds the transaction itself instead: pub_keys pairs each eligible input’s public key with the script_pub_key it spends, exactly as output_keys’s prv_keys does on the sending side, and every eligible input must be there for the same reason pub_key_sum there needs all of them. outpoints is the transaction’s, eligible or not: what the input hash binds to is the transaction, precisely as output_keys reads it.
Where the bindings serve secp256k1, this reaches silentpayments.scan_outputs with a prevouts_summary computed once from pub_keys and outpoints – the shape a wallet scanning a block needs, and the one issue btclib-org/btclib#910’s own measurement found 6.4x faster than the Python loop at a hundred outputs once a label is in play, which BIP352 asks every wallet to check (m = 0, the change label). Without a label the two arms are close, the Python one ahead at a hundred outputs, but that case is not what decided this: see the issue’s own comments for the numbers.
pub_key_sum and input_hash run unconditionally, before either arm is chosen, for the same reason output_keys computes a and h either way: a zero-sum refusal or an empty outpoint sequence gets the specific message those two functions already give it, rather than the bindings’ coarser one.
labels is label_lookup’s map – 33-byte label to 32-byte tweak, the bindings’ own spelling – and reaches the delegated arm unconverted; the Python arm, scan_outputs, reads the same bytes through scalar_from_prv_key.
Return scalar*point, the ECDH shared secret of BIP352.
One function for both ends, because there is one secret: the sender multiplies input_hash*a by the recipient’s B_scan, the recipient multiplies its b_scan by the tweak data input_hash*A, and commutativity is the protocol.
The public key stays octets rather than becoming a point: nothing here reads a coordinate of it, and mult_pub_key is that multiplication without the round trip through one. The point itself is the answer, which is why ecdh.shared_secret of the bindings is no substitute – it hashes, and BIP352 tags this point with a counter of its own;
btclib.ecc.dhhas that verdict for all four of the library’s ECDH-shaped computations.The octets arrive unproven, as ecc.ecies.derive_keys takes them: mult_pub_key’s own multiplication is the proof, refusing what is not a point of the curve, so proving them here would lift one x twice (issue btclib-org/btclib#887).
- btclib_wallet.silent_payments.tweak_data(outpoints: Sequence[OutPoint], A_sum: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) tuple[int, int][source]¶
Return input_hash*A_sum, the one value a scanner needs per tx.
BIP352’s Appendix A calls it the tweak data, and it is what a light client asks a server for: it is derived from the transaction alone, reveals nothing about any recipient, and a scanner multiplies it by its scan key to reach the shared secret. Which is why scan_outputs takes it rather than the outpoints – a light client never sees them.
btclib_wallet.slip132 module¶
SLIP132 address.
https://github.com/satoshilabs/slips/blob/master/slip-0132.md
- btclib_wallet.slip132.address_from_xkey(xkey: BIP32KeyData | bytes | str | bytearray | memoryview) str[source]¶
Return the SLIP132 base58/bech32 address.
The address is always derived from the compressed public key, as this is the default public key representation in BIP32.
- btclib_wallet.slip132.address_from_xpub(xpub: BIP32KeyData | bytes | str | bytearray | memoryview) str[source]¶
Return the SLIP132 base58/bech32 address.
The address is always derived from the compressed public key, as this is the default public key representation in BIP32.
- btclib_wallet.slip132.p2pkh_xkey(xkey: BIP32KeyData | bytes | str | bytearray | memoryview, der_path: str | Sequence[int] | int | bytes | bytearray | memoryview = 'm/44h/0h/0h', check_root_xkey: bool = True) str[source]¶
Return a p2pkh BIP32 xprv/xpub key at the derivation path.
btclib_wallet.tx_builder module¶
Compose the psbt spending a set of outputs, at a fee rate, with change.
Every part of this is elsewhere and nothing composes them, so every caller composes them again: Psbt holds what is being built, psbt.prevouts says what its inputs are worth, Psbt.vsize_estimate says how large the signed transaction will be, fee.fee_from_vsize prices that size and fee.dust_threshold says whether the change is worth creating. build_psbt is that composition, and the three decisions it makes are the ones a hand-written builder gets wrong.
The fee comes from the rate and the size, and the size comes from the psbt. A transaction that is not signed has no size to be priced by – Tx.vsize is read off a serialization, and the signatures are not written yet – so the object built here is a psbt: Psbt.vsize_estimate sizes the missing signatures from each input’s utxo and scripts, which is the one thing in the tree that answers before there is anything to sign. The unsigned transaction is built.psbt.tx, a property away, so a second entry point answering with a Tx would be a second spelling of one answer rather than a second answer.
Change is an output or it is fee. Below dust_threshold for its own script an output cannot be relayed, so it is not created and its value is left to the fee; and the transaction is then smaller than the one that was priced, so the fee it owes is computed again rather than reused. change_index is which output it is, or None for the branch that dropped it.
An input is a `PsbtIn`: the outpoint it spends, the output that outpoint names – witness_utxo or non_witness_utxo, whichever its kind of input takes – and whatever else says how it will be unlocked, a redeem script or a witness script included. Two things follow from taking the psbt’s own map rather than a pair of an outpoint and a TxOut. Nothing here fetches anything, an outpoint alone saying neither what it is worth nor what it spends, and a builder that fetches is a builder with a node in it; and an input whose script is wrapped or multisig is estimated exactly, its redeem or witness script being a field of the map that arrives rather than an argument this function would have to grow. The outputs are TxOut and not PsbtOut because the asymmetry is real: the input map is read, every byte a signature will take being computed from it, where nothing computed here reads an output map – what a wallet writes into one, descriptors’ update_psbt_output on the change output at change_index included, is written after this returns and changes no size.
Explicitly not here, both of them boundaries this library draws elsewhere too:
coin selection. Which utxos to spend is policy with a literature behind it, and keeping it out is what lets a caller bring its own; this spends the ones it is given, all of them. tx.input_weight is the number that caller prices a candidate with – one input, before there is a psbt to put it in – where what a fee is bought by here is the whole transaction, which Psbt.vsize_estimate is the one arithmetic for.
a node, an rpc, or wallet state. The same arguments give the same answer forever, which is fee’s own boundary: what is downstream of a network – a fee estimate for a confirmation target, which utxos are confirmed, the block height that would make an anti-fee-sniping lock time – is fed in rather than fetched.
- class btclib_wallet.tx_builder.FundedPsbt(psbt: Psbt, fee: int, change_index: int | None)[source]¶
Bases:
objectA psbt whose fee is paid, and what paying it decided.
The three answers Bitcoin Core’s fundrawtransaction gives, which is the same triple under its own names: the transaction, fee, and changepos – spelled here as an index into the psbt’s outputs, and None where Core writes -1 for the transaction that has no change output.
fee is what the inputs are worth less what the outputs hold. It is at least what fee_rate asked of the estimated size and can exceed it, by exactly the change that was too small to create.
- btclib_wallet.tx_builder.build_psbt(inputs: Sequence[PsbtIn], outputs: Sequence[TxOut], fee_rate: FeeRate, change_script_pub_key: bytes | str | bytearray | memoryview | None = None, *, tx_version: int = 2, lock_time: int = 0, dust_fee_rate: FeeRate = FeeRate(sats_per_kvbyte=3000), sizer: Callable[[PsbtIn, TxIn], list[int] | None] | None = None) FundedPsbt[source]¶
Return the psbt spending these inputs at this rate, and its change.
inputs are the psbt’s own input maps, each carrying the outpoint it spends and the output that outpoint names; outputs are what is being paid. What is left over pays the fee, and change_script_pub_key is where the rest of it goes – to an output of that script when it would be worth more than dust_threshold asks, and to the fee when it would not. No change script at all is every leftover satoshi to the fee, which is what a caller sweeping an address means and what a caller who forgot the argument gets, so it is spelled rather than defaulted.
Raised, all as BTClibValueError: no inputs, no outputs left to pay, an outpoint spent twice, an input carrying no utxo, an input whose type the psbt does not determine – psbt_size’s rule, and sizer is where a caller answers for one – and inputs that do not cover the outputs and the fee.
tx_version and lock_time are the transaction’s, defaulting to Core’s own 2 and to no lock time: the block height that would make a lock time worth setting is a node’s answer, and this function has no node. Each input’s sequence is its own PsbtIn.sequence, and an input naming none spends with the final sequence – no lock time and no BIP125 replacement, which a caller wanting either sets on the input rather than having overwritten here.
dust_fee_rate is the rate the dust threshold is computed at, Core’s -dustrelayfee default; it is not fee_rate, an output being dust by what the network will relay rather than by what this transaction chose to pay.
The psbt is version 0, which every Signer reads; Psbt.to_v2 is the other one.
btclib_wallet.tx_or_psbt module¶
One entry point for a transaction or a psbt, in whatever it arrives as.
A transaction copied from a block explorer is hex, a walletcreatefundedpsbt reply is base64, a QR code and a file are bytes – and which of Tx.parse, Psbt.parse and Psbt.b64decode applies is a question a caller holding one of them should not have to answer first. It is also a question with an unambiguous answer: BIP174’s five-byte <magic> is what a psbt begins with and what a transaction cannot, which is the very reason the 0xff is in it.
So this module sniffs, and delegates. The parsers stay whole – one refuses what does not deserialize, the other refuses what follows a psbt (issue btclib-org/btclib#179) – and no byte either of them reads is read here: merging the two into one lenient reader is how a dispatcher stops being a dispatcher, and it is the parsers that would pay for it.
It sits above tx and psbt rather than inside either, because its answer is one or the other and tx may not import psbt.
- btclib_wallet.tx_or_psbt.tx_or_psbt_from_any(data: bytes | str | bytearray | memoryview, *, check_validity: bool = True) Tx | Psbt[source]¶
Return the Psbt or the Tx the data holds, in whatever encoding.
hex, base64 or bytes; a Psbt when BIP174’s magic is what the bytes begin with, a Tx when it is not.
Module contents¶
The btclib_wallet package: what it publishes, and the version metadata.
Everything from a seed to a signed, broadcast transaction, and every module with a counterparty outside the process – a device, a node, a server, a file from another wallet – built on the protocol package btclib. The dependency points one way: this package imports btclib, and nothing in btclib imports this one. A name of this package is published here and only here: btclib does not re-export it.
__all__ is the root of the package’s public tree: the packages and top-level modules a caller reaches from this name, each of which declares its own __all__. A list rather than pkgutil.iter_modules, so that a module added to the directory does not publish itself.
name and the metadata dunders are not in it: each is still an attribute here, btclib_wallet.__version__ being how a caller reads the version.
Nothing is imported eagerly. A module is imported when it is first asked for, through the __getattr__ at the bottom of this file, so import btclib_wallet is the metadata lookup below and nothing else, and the import graph keeps its shape: tests/imports_test.py imports each module first, in a fresh interpreter, which an eager root would make impossible. What that costs is that mypy reads a module-level __getattr__ as a promise that any attribute may exist, so a misspelling on this package is a runtime AttributeError rather than a reported error; from btclib_wallet import bip32 and import btclib_wallet.bip32 resolve against the real modules and stay checked.