Source code for btclib_wallet.bip32.bip32

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""BIP32 hierarchical deterministic wallet functions.

https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

A deterministic wallet derives every key pair from a single root, the
one element requiring backup; BIP32 makes the derivation a tree, so a
branch of keys can be shared without the rest, and public derivation
computes child public keys with no private key at hand.

A BIP32 extended key is 78 bytes:

- [  : 4] version
- [ 4: 5] depth in the derivation path
- [ 5: 9] parent fingerprint
- [ 9:13] index
- [13:45] chain code
- [45:78] compressed pub_key or [0x00][prv_key]
"""

from __future__ import annotations

import functools
import hmac
from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING

from btclib import base58
from btclib.alias import BinaryData, Octets, Point, String
from btclib.curves import (
    Curve,
    bytes_from_point,
    bytes_from_prv_key_int,
    is_libsecp256k1_serving,
    is_x_coordinate_var,
    mult,
    point_from_octets,
    secp256k1,
)
from btclib.exceptions import BTClibTypeError, BTClibValueError, InvalidPrvKeyError
from btclib.hashes import hash160
from btclib.network import (
    NETWORKS,
    XPRV_VERSIONS_ALL,
    XPUB_VERSIONS_ALL,
    curve_from_xkeyversion,
    network_from_xkeyversion,
    xprvversions_from_network,
    xpubversion_from_xprvversion,
    xpubversions_from_network,
)
from btclib.utils import (
    assert_no_trailing,
    assert_type,
    bytes_from_octets,
    bytesio_from_binarydata,
    hex_string,
    int_from_json_number,
    is_integer,
)
from typing_extensions import override

from btclib_wallet.bip32.der_path import (
    _HARDENED_OFFSET,
    DerPath,
    indexes_from_der_path,
)

# the bindings, imported from their own package rather than through
# btclib's private re-export of it; None where they are not installed,
# which nothing calls: every call is behind `is_libsecp256k1_serving`,
# False in that configuration
try:
    from btclib_secp256k1 import keys as libsecp256k1_keys
except ImportError:  # pragma: no cover -- only an install without them
    libsecp256k1_keys = None  # type: ignore[assignment]

__all__ = [
    "BIP328_CHAIN_CODE",
    "BIP32Key",
    "BIP32KeyData",
    "crack_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_",
]

# secp256k1 is written out at each use rather than aliased to a module
# global "ec": BIP32 is defined for secp256k1 and for nothing else, so
# the alias was not configuration, and rebinding btclib_wallet.bip32.bip32.ec
# changed key validation for every caller in the process


_KEY_SIZE = [("version", 4), ("parent_fingerprint", 4), ("chain_code", 32), ("key", 33)]
_REQUIRED_LENGTH = 78


@functools.lru_cache(maxsize=2048)
def _cached_base58_decode(address: String) -> bytes:
    """Return `base58.decode(address)`, memoized on the xprv/xpub string.

    `derive` decodes its `xkey` argument fresh on every call, and a
    caller deriving many indices or paths from one account key -- what
    `derive_from_account` does once per address -- pays for decoding
    that same root again each time. `maxsize` is bounded rather than
    `None` for the reason `pedersen.second_generator`'s cache states:
    `address` is caller-supplied, and an unbounded cache on it would be
    a memory leak (issue btclib-org/btclib#287).

    2048 rather than the module's usual bare default (128) is measured on
    two workloads: btclib's own suite, and a caller that also calls
    `b58decode` directly once per derived key, on top of what `derive`
    already does (checksig-custody/checksig#643, the heavier of the two).
    Hit rate at 128, 512, 1024 and 2048 respectively: 22.2%, 24.3%, 27.5%
    and 28.8% for the first; 52.8%, 55.6%, 56.1% and 72.9% for the second
    -- most of the second workload's climb from 1024 to 2048, the first
    already flat by then.

    Past 2048 the second workload keeps climbing -- at 8192 it is still
    full and evicting -- but so does what stays resident: `address` may
    be an xprv, and every entry is a decoded key kept alive past its
    caller's own reference to it. 2048 is where the first workload has
    already flattened and the second has just cleared its own knee,
    rather than chasing a ratio neither workload's own shape asks for.

    Bytes, not the `BIP32KeyData` `b58decode` builds from them: frozen
    does not stop `object.__setattr__`, which
    `tests/bip32/bip32_test.py::test_assert_valid2` uses on purpose to
    corrupt independent instances decoded from the same string, so
    `b58decode` still has to construct a fresh one on every call. Bytes
    are the one result here nobody can mutate by accident.
    """
    return base58.decode(address)


def _assert_valid_depth_and_index(
    depth: int, index: int, parent_fingerprint: bytes
) -> None:
    """Raise an exception if depth, index and fingerprint disagree.

    The three are one check and not three: depth zero is the root, and a
    root has no parent, so it is the depth that decides what the index
    and the fingerprint of the parent are allowed to be.
    """
    if not 0 <= index <= 0xFFFFFFFF:
        raise BTClibValueError(f"invalid index: {index}")

    if not 0 <= depth <= 255:
        raise BTClibValueError(f"invalid depth: {depth}")

    if depth == 0:
        if parent_fingerprint != b"\x00" * 4:
            err_msg = "zero depth with non-zero parent fingerprint: "
            err_msg += f"0x{parent_fingerprint.hex()}"
            raise BTClibValueError(err_msg)
        if index != 0:
            raise BTClibValueError(f"zero depth with non-zero index: {index}")


def _assert_valid_key(version: bytes, key: bytes) -> None:
    """Raise an exception if the key is not valid for its version.

    The version is what says whether the 33 bytes are a private or a
    public key, so an unknown version leaves nothing to check them
    against and is the error itself.
    """
    if version in XPRV_VERSIONS_ALL:
        if key[0] != 0:
            raise BTClibValueError(f"invalid private key prefix: 0x{key[:1].hex()}")
        q = int.from_bytes(key[1:], byteorder="big", signed=False)
        if not 0 < q < secp256k1.n:
            # never echo the scalar: it is (attempted) key material
            raise BTClibValueError("invalid private key not in 1..n-1")
    elif version in XPUB_VERSIONS_ALL:
        if key[0] not in {2, 3}:
            err_msg = "invalid public key prefix not in (0x02, 0x03): "
            err_msg += f"0x{key[:1].hex()}"
            raise BTClibValueError(err_msg)
        # existence and nothing else, the y having been computed and
        # dropped: this is the caller is_x_coordinate_var's docstring
        # describes, and the square root it exists not to pay was paid
        # once per extended key, so by every level of every derivation
        # path (issue btclib-org/btclib#615). A predicate has no exception to
        # chain, so the message below states the error on its own, independent
        # of curve_group's own "invalid x-coordinate" wording
        x = int.from_bytes(key[1:], byteorder="big", signed=False)
        if not is_x_coordinate_var(x, secp256k1):
            raise BTClibValueError(f"invalid public key: 0x{key.hex()}")
    else:
        raise BTClibValueError(f"unknown extended key version: 0x{version.hex()}")


[docs] @dataclass(frozen=True, init=False) class BIP32KeyData: """A BIP32 extended key, decoded into its six fields. What one xprv/xpub string holds: version, depth, parent fingerprint, index, chain code and the 33-byte key, private keys carrying their 0x00 prefix. The wire form is the 78-byte serialize and parse; b58encode and b58decode add the customary Base58Check spelling. repr masks the key material of a private one. Frozen, so that a public function handed one can trust it rather than revalidate it: `check_validity=False` says "not checked yet", and a mutable field would let that stay true forever, one attribute write after the check that never came (issue btclib-org/btclib#727). `_BIP32KeyData` below is where derivation still needs to mutate one field at a time -- a sibling struct now, and not a subclass, frozen and non-frozen dataclasses refusing to mix in one inheritance chain. """ version: bytes depth: int parent_fingerprint: bytes # index is an int, not bytes, to avoid any byteorder ambiguity index: int chain_code: bytes key: bytes @property def is_private(self) -> bool: """Answer whether the key is private, by its 0x00 prefix.""" return self.key[0] == 0 @override def __repr__(self) -> str: # never echo private key material: mask key and chain_code # (key[:1], not is_private, so that repr cannot raise on an # invalid instance with an empty key) private = self.key[:1] == b"\x00" chain_code = "***" if private else self.chain_code.hex() key = "***" if private else self.key.hex() return ( f"{type(self).__name__}(" f"version={self.version.hex()}, " f"depth={self.depth}, " f"parent_fingerprint={self.parent_fingerprint.hex()}, " f"index={self.index}, " f"chain_code={chain_code}, " f"key={key})" ) @property def is_hardened(self) -> bool: """Answer whether the index is in the hardened range.""" return self.index >= _HARDENED_OFFSET @property def is_root(self) -> bool: """Answer whether this is a master key, no depth, index, parent.""" return ( self.depth == 0 and self.index == 0 and self.parent_fingerprint == b"\x00" * 4 ) def __init__( self, version: Octets, depth: int, parent_fingerprint: Octets, index: int, chain_code: Octets, key: Octets, *, check_validity: bool = True, ) -> None: object.__setattr__(self, "version", bytes_from_octets(version)) # a coercion, where the annotation already says int, for the same # reason bytes_from_octets is called on the four Octets fields: # from_dict feeds this constructor a json object, where a whole # number may arrive as a float. Coercing in assert_valid instead # would rewrite the object it is asked to inspect -- and it is # called by serialize() and to_dict(), so reading a key would # mutate it. A bool is refused rather than coerced: `true` out of # json is a schema error, not depth one object.__setattr__(self, "depth", int_from_json_number(depth, "depth")) object.__setattr__( self, "parent_fingerprint", bytes_from_octets(parent_fingerprint) ) object.__setattr__(self, "index", int_from_json_number(index, "index")) object.__setattr__(self, "chain_code", bytes_from_octets(chain_code)) object.__setattr__(self, "key", bytes_from_octets(key)) if check_validity: self.assert_valid()
[docs] def assert_valid(self) -> None: """Refuse what no valid extended key can hold. Field types and sizes, a depth consistent with index and parent fingerprint, a known version, and a key that parses -- as a scalar in 1..n-1 or as a point of the curve, whichever the version demands. """ for key, size in _KEY_SIZE: # bytes() is the type check, not a coercion: it raises # TypeError for a field rebound to a str, which would otherwise # pass the length test below and fail later on .hex(). The # result is never assigned back: validating must not rewrite # the object value = bytes(getattr(self, key)) if len(value) != size: err_msg = f"invalid {key} length: " err_msg += f"{len(value)} bytes" err_msg += f" instead of {size}" raise BTClibValueError(err_msg) # the same type check for the two int fields. Not a coercion, # which would repair the mistake by rewriting the object being # inspected; not dropped either, which would let a float reach # to_bytes and leave through an AttributeError for key in ("depth", "index"): value_ = getattr(self, key) if not is_integer(value_): err_msg = f"invalid {key} type: {type(value_).__name__}" raise BTClibTypeError(err_msg) # after the two loops and not before: these read the fields as # the bytes and the ints the loops have just established them to # be, and .hex() on a str field would raise AttributeError here _assert_valid_depth_and_index(self.depth, self.index, self.parent_fingerprint) _assert_valid_key(self.version, self.key)
[docs] def serialize(self, *, check_validity: bool = True) -> bytes: """Return the 78-byte serialization, BIP32's.""" if check_validity: self.assert_valid() return b"".join( [ self.version, self.depth.to_bytes(1, byteorder="big", signed=False), self.parent_fingerprint, self.index.to_bytes(4, byteorder="big", signed=False), self.chain_code, self.key, ] )
[docs] def b58encode(self, *, check_validity: bool = True) -> str: """Return the Base58Check text, the xprv/xpub spelling.""" data_binary = self.serialize(check_validity=check_validity) return base58.encode(data_binary).decode("ascii")
[docs] @classmethod def parse( cls: type[BIP32KeyData], xkey_bin: BinaryData, *, check_validity: bool = True ) -> BIP32KeyData: """Return a BIP32KeyData by parsing 78 bytes from binary data.""" stream = bytesio_from_binarydata(xkey_bin) key_bin = stream.read(_REQUIRED_LENGTH) # whatever check_validity says: seventy-eight bytes are what make # the slices below mean anything, where a semantic check is an # opinion about the key they carry. btclib/utils.py states the rule if len(key_bin) != _REQUIRED_LENGTH: err_msg = f"invalid decoded length: {len(key_bin)}" err_msg += f" instead of {_REQUIRED_LENGTH}" raise BTClibValueError(err_msg) assert_no_trailing(xkey_bin, stream, "extended key") return cls( version=key_bin[:4], depth=key_bin[4], parent_fingerprint=key_bin[5:9], index=int.from_bytes(key_bin[9:13], byteorder="big", signed=False), chain_code=key_bin[13:45], # the constant the read and the length check above are written # in, rather than a second spelling of it: the offsets are this # field's own, the total is the format's key=key_bin[45:_REQUIRED_LENGTH], check_validity=check_validity, )
[docs] @classmethod def b58decode( cls: type[BIP32KeyData], address: String, *, check_validity: bool = True ) -> BIP32KeyData: """Build a BIP32KeyData from its xprv/xpub Base58Check text. The type is asked here rather than left to `base58.decode`, which does ask it: the cache in front of that call keys on the argument, so anything unhashable -- a list, or a mutable buffer -- would leave a complaint about hashing instead of the refusal decoding would have given. """ assert_type(address, (str, bytes, bytearray, memoryview), "base58 string") if isinstance(address, str): address = address.strip() elif not isinstance(address, bytes): # copied for the cache and not for the decoding, which takes # a buffer as it comes: a bytearray is unhashable, and a # memoryview of one raises `ValueError: cannot hash writable # memoryview object` -- two different complaints, neither # about the address, where the copy answers both address = bytes(address) xkey_bin = _cached_base58_decode(address) return cls.parse(xkey_bin, check_validity=check_validity)
def _rootxprv_from_seed(seed: Octets, version: Octets) -> BIP32KeyData: """Return BIP32 root master extended private key from seed.""" seed = bytes_from_octets(seed) bit_length = len(seed) * 8 # the bit count is diagnostic enough: never echo the seed itself if bit_length < 128: raise BTClibValueError(f"too few bits for seed: {bit_length}") if bit_length > 512: raise BTClibValueError(f"too many bits for seed: {bit_length}") hmac_ = hmac.new(b"Bitcoin seed", seed, "sha512").digest() k = b"\x00" + hmac_[:32] v = bytes_from_octets(version, 4) return BIP32KeyData( version=v, depth=0, parent_fingerprint=b"\x00" * 4, index=0, chain_code=hmac_[32:], key=k, )
[docs] def rootxprv_from_seed_( seed: Octets, version: Octets = NETWORKS["mainnet"].bip32_prv ) -> BIP32KeyData: """Return the BIP32 root master extended private key of a seed. `rootxprv_from_seed` below is this with the Base58Check encoding on top, and the trailing underscore says which of the two this is: as in `ecc.dsa`, it marks the spelling for a caller holding the prepared form -- there a message already hashed, here the extended key itself rather than its xprv text. Which is what the four object spellings of this module are for. A caller deriving a child public key from a seed had btclib build Base58Check text and read it back at every hop, three encodings and three decodings of a spelling that never left the module: seed to `m/0h/1` and neutered costs about twice what the same three calls over `BIP32KeyData` do over distinct seeds, and not much less where one seed repeats and `_cached_base58_decode` answers (issue btclib-org/btclib#886). The text is what `rootxprv_from_seed` is for, and nothing here stops answering it. """ return _rootxprv_from_seed(seed, version)
[docs] def rootxprv_from_seed( seed: Octets, version: Octets = NETWORKS["mainnet"].bip32_prv ) -> str: """Return BIP32 root master extended private key from seed.""" return rootxprv_from_seed_(seed, version).b58encode()
#: A `BIP32KeyData`, or the base58 text -- xprv or xpub -- it encodes. BIP32Key = BIP32KeyData | String def _key_data_from_bip32_key(xkey: BIP32Key) -> BIP32KeyData: """Return the key as a valid BIP32KeyData, however it was spelled. What a public function taking a `BIP32Key` owes the private ones it calls: a string is decoded, and either spelling is validated, once and here. The private functions below then validate nothing, which is what the leading underscore says of them -- so a caller reaching past one of these wrappers owes its callee the same guarantee this provides. The key is validated as it stands, before `_derive` changes its depth: the depth-zero rule is a statement about the index and the parent fingerprint of *this* key, and at the final depth of a path it no longer says anything. """ if isinstance(xkey, BIP32KeyData): xkey.assert_valid() return xkey return BIP32KeyData.b58decode(xkey) def _xpub_from_xprv(xprv: BIP32KeyData) -> BIP32KeyData: """Neutered Derivation (ND). Derivation of the extended public key corresponding to an extended private key (“neutered” as it removes the ability to sign transactions). """ if xprv.key[0] != 0: # the offending key is public here, but never echo a # serialized xkey: the prefix already says what is wrong err_msg = f"not a private key: prefix 0x{xprv.key[:1].hex()}" raise BTClibValueError(err_msg) q = int.from_bytes(xprv.key[1:], byteorder="big", signed=False) return BIP32KeyData( version=xpubversion_from_xprvversion(xprv.version), depth=xprv.depth, parent_fingerprint=xprv.parent_fingerprint, index=xprv.index, chain_code=xprv.chain_code, key=bytes_from_prv_key_int(q), check_validity=False, )
[docs] def xpub_from_xprv_(xprv: BIP32Key) -> BIP32KeyData: """Neutered Derivation (ND), answering the extended key itself. `xpub_from_xprv` below is this with the Base58Check encoding on top; the trailing underscore is `rootxprv_from_seed_`'s, and says the same thing. """ xkey = _xpub_from_xprv(_key_data_from_bip32_key(xprv)) # the output check the text spelling got from `b58encode`, which is # what `serialize` inside it does: `_xpub_from_xprv` builds with # `check_validity=False`, so this is the one validation of the key # answered here -- and it is the whole of what the encoding was # validating, a tenth of what that encoding costs xkey.assert_valid() return xkey
[docs] def xpub_from_xprv(xprv: BIP32Key) -> str: """Neutered Derivation (ND). Derivation of the extended public key corresponding to an extended private key (“neutered” as it removes the ability to sign transactions). """ # check_validity=False: `xpub_from_xprv_` has just made that check on # the key it answered return xpub_from_xprv_(xprv).b58encode(check_validity=False)
[docs] def fingerprint(xkey: BIP32Key) -> bytes: """Return the four octets BIP32 identifies an extended key by. The first four of the HASH160 of the compressed public key, which is what BIP32 defines the fingerprint as and what a `BIP32KeyOrigin` names its master with. Here and not in a converter, because it is bip32's own idea: nothing outside this module's formats has a fingerprint (issue btclib-org/btclib#1188). Every reader of the four octets is this module's or sits above it -- `derive` below writes a parent's into each child it makes, `crack_prv_key_var` reads one back to tell whether a parent and a child are that pair, and `psbt`'s origins carry one to name a master. An xprv answers with its xpub's, the fingerprint being the public key's: a pair that gave two would identify one key twice. The neutering is `_xpub_from_xprv`'s, so the two spellings share the derivation rather than agreeing by coincidence. """ xkey_data = _key_data_from_bip32_key(xkey) if xkey_data.is_private: xkey_data = _xpub_from_xprv(xkey_data) return hash160(xkey_data.key)[:4]
[docs] def prv_keyinfo_from_xprv( xprv: BIP32Key, network: str | None = None, compressed: bool | None = None ) -> tuple[int, str, bool]: """Return the scalar, the network and the compression of an xprv. Here and not in a converter, for the reason `fingerprint` above is here: an extended key is this module's format, so the module that defines it is the one that reads it (issue btclib-org/btclib#1188). An extended key carries its own network and its own compression, so `network` and `compressed` are consistency checks and not choices -- a BIP32 key is always compressed, and its version bytes say which network claims it. `None` is "whatever the key says" for both. """ # None is a declared value here and means "whatever the key says", so # it is the one non-bool this position takes if compressed is not None: assert_type(compressed, bool, "compressed") xprv_data = _key_data_from_bip32_key(xprv) # a BIP32 key is always compressed, so a caller asking for # uncompressed is asking about another format. This follows the # decode rather than preceding it, so that what it answers is a fault # in a key that did decode, where text that is no xkey has already # left as the decode's own refusal if compressed is not None and not compressed: raise InvalidPrvKeyError("uncompressed SEC / compressed BIP32 mismatch") # from here it is an xkey, so what follows is a fault in one if xprv_data.key[0] != 0: # the offending key is public here, but never echo a # serialized xkey: the prefix already says what is wrong err_msg = f"not a private key: prefix 0x{xprv_data.key[:1].hex()}" raise InvalidPrvKeyError(err_msg) if network is None: network = network_from_xkeyversion(xprv_data.version) allowed_versions = xprvversions_from_network(network) if xprv_data.version not in allowed_versions: # never echo the xprv, which is a private key: # the version is the mismatching, non-secret, part err_msg = f"not a {network} key: version 0x{xprv_data.version.hex()}" raise InvalidPrvKeyError(err_msg) q = int.from_bytes(xprv_data.key[1:], byteorder="big") return q, network, True
[docs] def pub_keyinfo_from_xpub( xpub: BIP32Key, network: str | None = None, compressed: bool | None = None ) -> tuple[bytes, str]: """Return the SEC octets and the network of an xpub. `prv_keyinfo_from_xprv` above for a public extended key, and its `network` and `compressed` mean the same thing here. An xprv is refused rather than neutered: this answers about the key it is handed, and `pub_keyinfo_from_xkey` below is the spelling for a caller that holds either half. """ if compressed is not None: assert_type(compressed, bool, "compressed") xpub_data = _key_data_from_bip32_key(xpub) # `prv_keyinfo_from_xprv`'s refusal above, on the public half, and # after the decode for the reason stated there if compressed is not None and not compressed: raise BTClibValueError("uncompressed SEC / compressed BIP32 mismatch") return _pub_keyinfo_from_xpub(xpub_data, network)
def _pub_keyinfo_from_xpub( xpub_data: BIP32KeyData, network: str | None ) -> tuple[bytes, str]: """`pub_keyinfo_from_xpub` on a key already decoded and validated. Split off because `pub_keyinfo_from_xkey` below asks for it: the key it passes is the one `_xpub_from_xprv` has just built, so decoding it again through the public spelling would validate a key this module made, and `_assert_valid_key`'s on-curve test is most of what validating an extended public key costs. `prv_keyinfo_from_xprv` above has no such second caller and is one function. No `compressed`: the argument is a consistency check `pub_keyinfo_from_xpub` makes on the key it has decoded, before calling here. """ if xpub_data.key[0] not in {2, 3}: # this branch is reached with an xprv: never echo it, # the prefix already says what is wrong err_msg = f"not a public key: prefix 0x{xpub_data.key[:1].hex()}" raise BTClibValueError(err_msg) if network is None: return xpub_data.key, network_from_xkeyversion(xpub_data.version) allowed_versions = xpubversions_from_network(network) if xpub_data.version not in allowed_versions: # an xpub is not funds-critical, but it derives all child pub # keys: keep it out of exception messages (and logs) too err_msg = f"not a {network} key: version 0x{xpub_data.version.hex()}" raise BTClibValueError(err_msg) return xpub_data.key, network
[docs] def pub_keyinfo_from_xkey( xkey: BIP32Key, network: str | None = None ) -> tuple[bytes, str]: """Return the SEC octets and the network of an extended key, either half. The public key of an xprv is the public key of the xpub it neuters to, which is `fingerprint` above reading the same material the same way: a pair that answered two different public keys would not be a pair. So an address, a script and a key origin all ask this rather than asking whether they were handed the private half. Here and not in a converter, for the reason `prv_keyinfo_from_xprv` above is here: an extended key is this module's format (issue btclib-org/btclib#1188). An xprv costs the scalar multiplication `_xpub_from_xprv` performs, and an xpub costs nothing beyond the decode. No `compressed`: a BIP32 key is always compressed, so the only value that argument could carry is the one the key already states. """ xkey_data = _key_data_from_bip32_key(xkey) if xkey_data.is_private: xkey_data = _xpub_from_xprv(xkey_data) return _pub_keyinfo_from_xpub(xkey_data, network)
[docs] def point_from_xpub(xpub: BIP32Key, ec: Curve = secp256k1) -> Point: """Return the curve point an xpub's 33 octets are. `ec` is compared against the curve of the network the version bytes name rather than used to parse with, so a caller asking about a curve the key is not of is told which of the two disagrees. """ assert_type(ec, Curve, "ec") xpub_data = _key_data_from_bip32_key(xpub) if xpub_data.is_private: # never echo the key, which is private here: # the prefix already says what is wrong raise BTClibValueError(f"not a public key: prefix 0x{xpub_data.key[:1].hex()}") ec2 = curve_from_xkeyversion(xpub_data.version) if ec != ec2: err_msg = f"ec/xpub version ({xpub_data.version.hex()}) mismatch" raise BTClibValueError(err_msg) return point_from_octets(xpub_data.key, ec)
# repr=False: a generated __repr__ would print key and prv_key_int, the # private scalar and the key material BIP32KeyData's own masking repr # exists to hide -- this struct has no repr of its own to mask with, so # it takes the default's silence instead @dataclass(repr=False) class _BIP32KeyData: """The mutable working copy the derivation loop rewrites in place. Not a `BIP32KeyData`, and deliberately: that class is frozen so a public function can trust one it is handed (issue btclib-org/btclib#727), and `dataclasses` refuses a mutable dataclass inheriting from a frozen one. `__prv_key_derivation`, `__pub_key_derivation` and the two path-walkers below mutate `chain_code`, `key`, `prv_key_int` and `parent_fingerprint` field by field across up to 255 levels of a path, for the measured performance reasons their own comments give; `_derive` builds one, mutates it, and hands a real `BIP32KeyData` back built from its final fields -- one allocation for the whole path, not one per level. Never validates and never coerces: every field always comes from a `BIP32KeyData` already valid or from this same loop, so there is nothing here for `check_validity` to gate. `prv_key_int` is the one intermediate result multi-level derivation reuses: do not rely on it elsewhere. The public counterpart a private key needs is not cached beside it -- `__prv_key_path_derivation` computes it once, for the fingerprint, and hands it to the step that would recompute it; a point cached here would instead be built for every key, including the public ones that never look at it. """ version: bytes depth: int parent_fingerprint: bytes index: int chain_code: bytes key: bytes prv_key_int: int # non-zero for private key only @property def is_private(self) -> bool: """Answer whether the key is private, by its 0x00 prefix.""" return self.key[0] == 0 # the group order as the 32 octets a scalar is written in: big-endian # bytes of one width compare as the numbers they spell, so the range # check BIP32 makes of the hmac's left half is made where the hash left # it, and neither the check nor the tweak that follows reads an integer # out of it _N_BYTES = secp256k1.n.to_bytes(secp256k1.n_size, byteorder="big") def _invalid_child(index: int, reason: str) -> BTClibValueError: """Return the error for a child BIP32 declares invalid. BIP32 has three of these -- parse256(IL) >= n, a zero private child, a public child at infinity -- and tells the caller to "proceed with the next value for i". Raising says so rather than deriving that next index silently: this code is asked for one index, and returning the key of another is a substitution nothing downstream could detect. Bitcoin Core answers the same way, CKDpriv returning false and leaving the choice to whoever picked the path. At odds of about 2^-127 none of the three is reachable, so what this buys is a defined answer rather than a key no other wallet derives. """ err_msg = f"invalid child index {index}: {reason}" err_msg += "; BIP32 mandates deriving the next index instead" return BTClibValueError(err_msg) def __prv_key_derivation(xkey: _BIP32KeyData, index: int, pub_key: bytes) -> None: xb = ( xkey.key if index >= _HARDENED_OFFSET else pub_key or bytes_from_prv_key_int(xkey.prv_key_int) ) xb += index.to_bytes(4, byteorder="big", signed=False) hmac_ = hmac.new(xkey.chain_code, xb, "sha512").digest() offset = hmac_[:32] if offset >= _N_BYTES: raise _invalid_child(index, "the hmac left half is not a valid scalar") # the sum of two scalars, one of them the parent private key: # secp256k1_ec_seckey_tweak_add computes it in constant time, where # Python's own `(a + b) % n` is variable in time with the operands # and leaves an unzeroized copy of each intermediate behind. The key # goes in as the 32 bytes it is stored as rather than as # xkey.prv_key_int, so that no arithmetic on the secret happens # here; the foreign call costs more than the Python sum does, and # both are lost in the hmac and public key of the derivation around # them. # # The dispatch is not on the curve, BIP32 being defined for secp256k1 # alone: it is on whether the bindings are there to serve it, which is # the one question `is_libsecp256k1_serving` answers that another curve # would not have asked. What the Python arm below is for is a caller # without them, and SECURITY.md says of it what it says of every # Python path: the arithmetic is on integers and is not constant-time if is_libsecp256k1_serving(): try: key = libsecp256k1_keys.prvkey_tweak_add(xkey.key[1:], offset) except ValueError as e: # past the range check above, the one sum libsecp256k1 refuses # is the zero BIP32 refuses too raise _invalid_child(index, "the child private key is zero") from e prv_key_int = int.from_bytes(key, byteorder="big", signed=False) else: # ki = parse256(IL) + kpar (mod n), which is BIP32's own equation prv_key_int = ( xkey.prv_key_int + int.from_bytes(offset, byteorder="big", signed=False) ) % secp256k1.n if prv_key_int == 0: raise _invalid_child(index, "the child private key is zero") key = prv_key_int.to_bytes(32, byteorder="big", signed=False) # xkey is mutated only past the checks, so a rejected index leaves # it the key it was rather than half a derivation xkey.chain_code = hmac_[32:] xkey.prv_key_int = prv_key_int xkey.key = b"\x00" + key def _pub_key_offset(chain_code: bytes, key: bytes, index: int) -> tuple[bytes, bytes]: """Return what an unhardened index adds, and the child chain code. The scalar alone, without the point it is added to: BIP32 derives a public child as P + IL*G, so this is the whole of the arithmetic that a caller which is not deriving a key of its own needs -- `pub_key_derivation_tweaks` below, and through it the MuSig2 aggregate keys of BIP328, which have no private key to derive with. The 32 octets of the hmac's left half, not the integer they spell: what reads them is `keys.pubkey_tweak_add`, which takes octets, and what `pub_key_derivation_tweaks` answers is octets too, so an integer here would be read out of the hash and written straight back. """ xb = key + index.to_bytes(4, byteorder="big", signed=False) hmac_ = hmac.new(chain_code, xb, "sha512").digest() offset = hmac_[:32] if offset >= _N_BYTES: raise _invalid_child(index, "the hmac left half is not a valid scalar") return offset, hmac_[32:] class _PythonPubKeyTweakChain: """What `keys.PubkeyTweakChain` is, in the arithmetic of this library. The other half of `_pub_key_offset` above: that answers the scalar a step adds, this adds it to the point. A path is a chain because each step's child is the next step's parent, so the point is held from one step to the next rather than parsed again out of the octets the step before it has just written -- which is what the bindings' class holds it for, and is worth more here: `point_from_octets` on a compressed key is a modular square root, far dearer than their own parse, so a path of any length pays one instead of one per level. `curves` has the same arithmetic twice over -- `tweak_add_var` ends in `ec.add_var(P, mult(t, ec.G, ec))`, and `TweakChain` is a point with many tweaks of it and the bindings' chain where they serve -- and this is not written here for want of noticing. Three things differ, and each is the whole of a step: `TweakChain`'s tweaks are absolute, measured from the point it was built on, where BIP32's are successive; `tweak_add_var` pays a `require_on_curve` per step, on a point this one has just computed itself; and a refused tweak drops `TweakChain` to the one-shot pair and it answers the next one, where BIP32 has to end the path instead. The contract is theirs, so that `_pub_key_tweak_chain` below can answer either and its callers need not know which: `tweak_add` answers the serialized child key and raises `ValueError` for the sum at infinity -- which libsecp256k1 has no public key for and BIP32 refuses too, `_invalid_child` being what the callers turn it into -- and the constructor raises it for octets that are no point. Theirs for every argument a caller here passes, and `compressed` is where the two would part if one did not: `bytes_from_point` refuses a value that is not a bool and their serialization takes it for the flag it is truthy as. This one is the stricter, and stricter is the side to be on -- but it is what makes the order below matter, so it is written here rather than left to be rediscovered from a raise the other implementation does not make. Both callers pass the literal, `compressed` is typed `bool`, and the union alias is what has mypy hold them to it. A refused step ends the chain, which is that contract too: "pubkey will be set to an invalid value if this function returns 0" is what libsecp256k1 says of `secp256k1_ec_pubkey_tweak_add`, and that pubkey is what their chain holds. So there is nothing to step from afterwards and neither implementation defines what a further step answers; both callers here raise instead of asking again, a refused index ending the whole derivation. Python throughout, and it has to be: with libsecp256k1 in reach their class is the better answer, so a chain that mixed the two would be the one shape never worth building. It cannot mix as written -- `mult` and the lift inside `point_from_octets` gate on the state `is_libsecp256k1_serving` reads, which is the call that chose this implementation, and cannot answer it differently -- and `test_the_py_arm_reaches_no_bindings` checks that rather than trusting it, a dispatch made finer-grained one day being what would end it silently. """ def __init__(self, key: bytes) -> None: self._point = point_from_octets(key, secp256k1) def tweak_add(self, tweak: bytes, compressed: bool = True) -> bytes: """Return the key of point + tweak*G, and step the chain to it.""" # Ki = point(parse256(IL)) + Kpar, which is BIP32's own equation point = secp256k1.add_var( self._point, mult(int.from_bytes(tweak, byteorder="big", signed=False)) ) if not point[1]: # y == 0 is how an affine point spells infinity here, its x # being arbitrary -- INF is (5, 0), and `curve_group`'s # add_aff_var says why: the group has one point more than a # pair of field elements can name, so no formula reaches that # spelling and the one real point with y == 0, a two-torsion # point, has no affine form at all. Comparing with INF would # test the arbitrary x as well raise BTClibValueError("the sum is the point at infinity") # serialized before the step is taken, as __prv_key_derivation # mutates only past its own checks: `compressed` is read by # bytes_from_point, which refuses a value that is not a bool, and # a chain left stepped by a call that raised would answer the # next tweak from a point its caller never received sec = bytes_from_point(point, secp256k1, compressed) self._point = point return sec # the two implementations of one chain: what a caller of the function # below holds, and neither of them a state the other has to allow for. # # Under TYPE_CHECKING, because the union is the annotation of two # signatures and of nothing else: `from __future__ import annotations` # leaves an annotation a string, while a union built at module level is # an object, and building this one would ask the bindings for a class # where they may not be installed. Nothing calls `get_type_hints` on # either signature, which is what would want the name back at runtime if TYPE_CHECKING: _PubKeyTweakChain = libsecp256k1_keys.PubkeyTweakChain | _PythonPubKeyTweakChain def _pub_key_tweak_chain(key: bytes) -> _PubKeyTweakChain: """Return the chain a public derivation path is walked with. The dispatch, made once for a whole path rather than at every step of it: BIP32 is defined for secp256k1 alone, so what is asked here is not which curve this is but whether the bindings are there to serve it, which is the one question `is_libsecp256k1_serving` answers that another curve would not have asked. """ return ( libsecp256k1_keys.PubkeyTweakChain(key) if is_libsecp256k1_serving() else _PythonPubKeyTweakChain(key) ) # BIP328's chain code, which is the sha256 of "MuSig2MuSig2MuSig2": an # aggregate key has no chain code of its own, so a synthetic xpub is the # key plus this constant, and every implementation deriving from an # aggregate key has to use the same one or derive other children. It sits # beside the tweaks below because it is the other half of one fact: the # derivation BIP328 defines is this chain code walked by that function, # and both `psbt.musig2` and a `musig()` descriptor key expression need it BIP328_CHAIN_CODE = bytes.fromhex( "868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965" )
[docs] def pub_key_derivation_tweaks( pub_key: Octets, chain_code: Octets, der_path: DerPath ) -> list[bytes]: """Return the 32-byte tweak each step of a public derivation adds. A public child is the parent point plus IL*G, so a whole path is a list of scalars that can be applied wherever the point itself is not available to derive from. That is what a BIP327 MuSig2 aggregate key needs: the group has no private key, so BIP328 derivation reaches the signers as plain tweaks of the aggregate key, and BIP373 carries a psbt signed under a key derived that way. Hardened indexes are refused before any step is walked, as __pub_key_path_derivation refuses them: they need a private key that by construction does not exist. """ key = bytes_from_octets(pub_key, secp256k1.p_size + 1) code = bytes_from_octets(chain_code, 32) indexes = indexes_from_der_path(der_path) if any(index >= _HARDENED_OFFSET for index in indexes): raise BTClibValueError("invalid hardened derivation from public key") # one parse for the whole path rather than one per index: each step # still needs its own serialized key, to hash into the next tweak, # but not a fresh parse of the bytes the step before it just # serialized -- the chain holds the point in between. # Outside the loop and not inside an `if indexes:`, so that a path of # no steps is the one spelling of this call that still looks at the # key it was handed: [] is the right answer for it, and the right # answer for 33 bytes that are not a point is no answer try: chain = _pub_key_tweak_chain(key) except ValueError as e: raise BTClibValueError(f"invalid public key: {key.hex()}") from e tweaks: list[bytes] = [] for index in indexes: offset, code = _pub_key_offset(code, key, index) tweaks.append(offset) key = chain.tweak_add(offset, compressed=True) return tweaks
def __pub_key_derivation( xkey: _BIP32KeyData, index: int, chain: _PubKeyTweakChain ) -> None: offset, chain_code = _pub_key_offset(xkey.chain_code, xkey.key, index) # the parent point plus the generator times the offset: one step is # an order of magnitude cheaper where secp256k1_ec_pubkey_tweak_add # computes it than where `add_var(P, mult(t))` does, which is what a # caller without the bindings pays here -- the dispatch being off # altogether on that arm, so the multiplication is Python as well. # xkey holds the key serialized throughout on the delegated one: no # point of btclib's own is ever built, ffi.new's raw C struct staying # inside the bindings. # Compressed on the way out, which is the spelling an extended key # holds: asking for the uncompressed form to read a y out of it costs # a second serialize and the parity arithmetic that follows, for a # coordinate nothing here goes on to use. `chain` is __pub_key_path_ # derivation's, held across every index of the path so that only the # first of them pays for parsing xkey.key rather than every one -- # and which of the two it holds is its own business try: sec = chain.tweak_add(offset, compressed=True) except ValueError as e: # past the range check above, the one sum the chain refuses is # the point at infinity BIP32 refuses too raise _invalid_child( index, "the child public key is the point at infinity" ) from e xkey.chain_code = chain_code xkey.key = sec def __prv_key_path_derivation(xkey: _BIP32KeyData, indexes: list[int]) -> None: """Derive xkey down the whole path, privately. The last index is derived apart from the others because the fingerprint a child carries is its parent's: it is taken from the key as it stands one derivation short of the end. The public key computed for it is then handed to that last derivation, which for an unhardened index would otherwise compute it a second time. """ for index in indexes[:-1]: __prv_key_derivation(xkey, index, b"") pub_key = bytes_from_prv_key_int(xkey.prv_key_int) xkey.parent_fingerprint = hash160(pub_key)[:4] __prv_key_derivation(xkey, indexes[-1], pub_key) def __pub_key_path_derivation(xkey: _BIP32KeyData, indexes: list[int]) -> None: """Derive xkey down the whole path, publicly. A hardened index needs the private key, so the whole path is refused before any of it is walked: half a derivation would leave the caller holding a key at neither end of the path. """ if any(index >= _HARDENED_OFFSET for index in indexes): raise BTClibValueError("invalid hardened derivation from public key") chain = _pub_key_tweak_chain(xkey.key) for index in indexes[:-1]: __pub_key_derivation(xkey, index, chain) xkey.parent_fingerprint = hash160(xkey.key)[:4] __pub_key_derivation(xkey, indexes[-1], chain) def _force_version(version: bytes, forced_version: Octets) -> bytes: """Return the forced version, which must be of the key's own kind. xprv to yprv is the same key spelled for another script type, while xprv to ypub would claim the key is public with the bytes still private -- neutering is _xpub_from_xprv and not a relabelling. """ allowed_versions = ( XPRV_VERSIONS_ALL if version in XPRV_VERSIONS_ALL else XPUB_VERSIONS_ALL ) fversion = bytes_from_octets(forced_version, 4) if fversion not in allowed_versions: err_msg = "invalid version forced on the extended key" err_msg += f"{hex_string(fversion)}" raise BTClibValueError(err_msg) return fversion def _derive( xkey: BIP32KeyData, der_path: DerPath, forced_version: Octets | None ) -> BIP32KeyData: indexes = indexes_from_der_path(der_path) final_depth = xkey.depth + len(indexes) if final_depth > 255: err_msg = f"final depth greater than 255: {final_depth}" raise BTClibValueError(err_msg) # the mutable working copy the loop below rewrites field by field: # the six fields come from a key the caller has validated, and the # one that changes here -- the depth -- is bounded on the line above working = _BIP32KeyData( version=xkey.version, depth=final_depth, parent_fingerprint=xkey.parent_fingerprint, index=xkey.index, chain_code=xkey.chain_code, key=xkey.key, prv_key_int=( int.from_bytes(xkey.key[1:], "big", signed=False) if xkey.is_private else 0 ), ) if forced_version: working.version = _force_version(working.version, forced_version) if indexes: if working.is_private: __prv_key_path_derivation(working, indexes) else: __pub_key_path_derivation(working, indexes) working.index = indexes[-1] # `check_validity=False`: validating here would also ask the # depth-zero rule about the depth this just walked to, where it no # longer means anything; the one validation the wrapper is entitled # to is its own, on what this hands back return BIP32KeyData( version=working.version, depth=working.depth, parent_fingerprint=working.parent_fingerprint, index=working.index, chain_code=working.chain_code, key=working.key, check_validity=False, )
[docs] def derive_( xkey: BIP32Key, der_path: DerPath, forced_version: Octets | None = None ) -> BIP32KeyData: """Derive a BIP32 key across a path, answering the extended key itself. `derive` below is this with the Base58Check encoding on top; the trailing underscore is `rootxprv_from_seed_`'s, and says the same thing. It is the one of the four object spellings the measurement of issue btclib-org/btclib#886 is about: a caller deriving from a key it holds pays neither the decoding of the argument nor the encoding of the answer, where the xprv text it would have built is a string the next call decodes again. The path and the version are `derive`'s, and so is what they accept. """ derived = _derive(_key_data_from_bip32_key(xkey), der_path, forced_version) # the output check the wrapper is entitled to, and the only validation # of the derived key: `derive` calls `b58encode(check_validity=False)`, # skipping the check `serialize` would otherwise run, so this is where # it happens, at a tenth of what the encoding costs derived.assert_valid() return derived
[docs] def derive( xkey: BIP32Key, der_path: DerPath, forced_version: Octets | None = None ) -> str: """Derive a BIP32 key across a path spanning multiple depth levels. Valid DerPath examples: - string like "m/44h/0'/1H/0/10" - iterable integer indexes - one single integer index - bytes, bytearray or memoryview in multiples of the 4-bytes index DerPath is case/blank/extra-slash insensitive (e.g. "M /44h / 0' /1H // 0/ 10 / "). """ # check_validity=False: `derive_` has just made that check on the key # it answered return derive_(xkey, der_path, forced_version).b58encode(check_validity=False)
def _assert_valid_branch( mxkey: BIP32KeyData, branch: int, branches_0_1_only: bool, max_index: int ) -> None: if not mxkey.is_hardened: raise BTClibValueError("unhardened account/master key") # prepared is not unchecked: `branch` is a bare int rather than an # Integer, so `is_integer`'s policy has to be asked of it directly # instead of arriving through `int_from_integer` (issue # btclib-org/btclib#1403, CONTRIBUTING.md's "This repository in particular") if not is_integer(branch): raise BTClibTypeError(f"non-integer branch: {branch}") # the bound both `branch` and, at every call site below, # `_assert_valid_address_index`'s own `address_index` compare # against -- no less a bare int than either: `max_index=True` is a # bound of one, silently narrowing what is accepted rather than # raising (issue btclib-org/btclib#1413). Checked once, here, since this # function runs first at every call site and on the same `max_index` if not is_integer(max_index): raise BTClibTypeError(f"non-integer max_index: {max_index}") if branch >= _HARDENED_OFFSET: raise BTClibValueError("invalid private derivation at branch level") if branch > max_index: err_msg = f"invalid branch number: {branch} is higher than {max_index}." raise BTClibValueError(err_msg) if branches_0_1_only and branch not in {0, 1}: raise BTClibValueError(f"invalid branch number: {branch} not in (0, 1)") def _assert_valid_address_index(address_index: int, max_index: int) -> None: # same reasoning as `_assert_valid_branch`'s own `is_integer` check. # `max_index` is not re-checked here: both call sites below run # `_assert_valid_branch` first, on the same `max_index`, which is # where that check lives (issue btclib-org/btclib#1413) -- a second one here # could never be reached and the coverage floor is what catches that if not is_integer(address_index): raise BTClibTypeError(f"non-integer address index: {address_index}") if address_index >= _HARDENED_OFFSET: raise BTClibValueError("invalid private derivation at address index level") if address_index > max_index: err_msg = f"invalid address index: {address_index} is higher than {max_index}." raise BTClibValueError(err_msg) def _derive_from_account( mxkey: BIP32KeyData, branch: int, address_index: int, branches_0_1_only: bool, max_index: int, ) -> BIP32KeyData: _assert_valid_branch(mxkey, branch, branches_0_1_only, max_index) _assert_valid_address_index(address_index, max_index) return _derive(mxkey, f"m/{branch}/{address_index}", None)
[docs] def derive_from_account_( mxkey: BIP32Key, branch: int, address_index: int, branches_0_1_only: bool = True, max_index: int = 0xFFFF, ) -> BIP32KeyData: """Derive at the given branch and index, answering the extended key. `derive_from_account` below is this with the Base58Check encoding on top; the trailing underscore is `rootxprv_from_seed_`'s, and says the same thing. Which is the spelling a wallet wants: `key_wallet` and `script_wallet` derive one of these per address, and an address is built from the key rather than from its text. """ derived = _derive_from_account( _key_data_from_bip32_key(mxkey), branch, address_index, branches_0_1_only, max_index, ) # the output check, as in `derive_` above and for its reason derived.assert_valid() return derived
[docs] def derive_from_account_range_( mxkey: BIP32Key, branch: int, address_indexes: Sequence[int], branches_0_1_only: bool = True, max_index: int = 0xFFFF, ) -> list[BIP32KeyData]: """Derive many addresses of one branch, walking to it once. `derive_from_account_` above answers one, and a wallet asks for many: a gap-limit scan, a ranged descriptor, an account being enumerated. Asked one at a time, each of those walks `m/branch/index` from the account key, so the branch level -- which every sibling shares -- is derived again for every one of them, hmac and tweak and all. Here it is derived once and each index is one level on top of it, at a little over half the cost per address, measured over a thousand. Which is the larger half of what issue btclib-org/btclib#918 asked about, and not the half it named. The parse of the account key is one per *path* and not one per level -- `_PubKeyTweakChain` holds the point across the levels of a walk, so the branch key is never parsed from octets -- so what a range saves there is six percent of an address, which is not worth an entry point. The level is worth one. A sequence rather than a first and a count, so that a scan resuming at a gap, or a descriptor's own list, is the argument itself. One address is a loss -- the branch walked and nothing to amortize it over -- and two already pay, so a caller with exactly one still wants `derive_from_account_`. Every index is refused by the rules `derive_from_account_` refuses it by, before any of them is walked: a list half derived would leave the caller holding the addresses before the bad index and no answer for the rest. The branch is refused whatever the list, an empty one included -- a branch that is no branch is a bad call and not a question nobody asked -- and is *walked* only where there is an index to put on it. """ account = _key_data_from_bip32_key(mxkey) _assert_valid_branch(account, branch, branches_0_1_only, max_index) for address_index in address_indexes: _assert_valid_address_index(address_index, max_index) # no index is no addresses, and the branch it would have been reached # through is a level nobody asked for: the validation above still # runs, a branch that is no branch being a bad call whatever the list if not address_indexes: return [] # check_validity=False: the branch key is not the answer and is not # handed back, and the depth-zero rule would be asked of a depth this # has just walked to -- as `_derive` says of its own return branch_key = _derive(account, f"m/{branch}", None) derived = [_derive(branch_key, f"m/{index}", None) for index in address_indexes] # the output check, once per key, as the single-address spelling makes # it on the one key it answers for key in derived: key.assert_valid() return derived
[docs] def derive_from_account_range( mxkey: BIP32Key, branch: int, address_indexes: Sequence[int], branches_0_1_only: bool = True, max_index: int = 0xFFFF, ) -> list[str]: """Derive many addresses of one branch, as Base58Check text.""" # check_validity=False: `derive_from_account_range_` has just checked return [ key.b58encode(check_validity=False) for key in derive_from_account_range_( mxkey, branch, address_indexes, branches_0_1_only, max_index ) ]
[docs] def derive_from_account( mxkey: BIP32Key, branch: int, address_index: int, branches_0_1_only: bool = True, max_index: int = 0xFFFF, ) -> str: """Derive a key with public derivation at the given branch and index. It also ensures that the master key is hardened, that the branch is a standard receive or change, and that the index is not arbitrarily high. """ # check_validity=False: `derive_from_account_` has just made that check return derive_from_account_( mxkey, branch, address_index, branches_0_1_only, max_index ).b58encode(check_validity=False)
[docs] def crack_prv_key_var(parent_xpub: BIP32Key, child_xprv: BIP32Key) -> str: """Return the parent xprv from a parent xpub and a non-hardened child. The known break BIP32 warns about: a non-hardened child's private key minus the derivation offset -- computable from the xpub -- is the parent's, so leaking one child xprv beside the account xpub leaks the account. A hardened child is refused, its offset not being computable. """ # both arguments through the one place a BIP32Key becomes a validated # BIP32KeyData: this function exists to demonstrate a known BIP32 # weakness, so an answer it gives for a child its own assert_valid # refuses is a wrong lesson. Frozen, so aliased rather than copied -- # there is nothing in it for a copy to protect any more p = _key_data_from_bip32_key(parent_xpub) if p.key[0] not in {2, 3}: raise BTClibValueError(_err_msg("parent", "not a public", p)) c = _key_data_from_bip32_key(child_xprv) if c.key[0] != 0: raise BTClibValueError(_err_msg("child", "not a private", c)) # check depth if c.depth != p.depth + 1: raise BTClibValueError("not a parent's child: wrong depths") # check fingerprint if c.parent_fingerprint != hash160(p.key)[:4]: raise BTClibValueError("not a parent's child: wrong parent fingerprint") if c.is_hardened: raise BTClibValueError("hardened child derivation") hmac_ = hmac.new( p.chain_code, p.key + c.index.to_bytes(4, byteorder="big", signed=False), "sha512", ).digest() child_q = int.from_bytes(c.key[1:], byteorder="big", signed=False) offset = int.from_bytes(hmac_[:32], byteorder="big", signed=False) parent_q = (child_q - offset) % secp256k1.n parent = BIP32KeyData( version=c.version, depth=p.depth, parent_fingerprint=p.parent_fingerprint, index=p.index, chain_code=p.chain_code, key=b"\x00" + parent_q.to_bytes(32, byteorder="big", signed=False), check_validity=False, ) return parent.b58encode()
def _err_msg( child_or_parent: str, not_a_private_or_public: str, key: BIP32KeyData ) -> str: # the "not a public" branch is reached with an xprv: # never echo a serialized xkey, the prefix says what is wrong return ( f"extended {child_or_parent} key is {not_a_private_or_public} key: " f"prefix 0x{key.key[:1].hex()}" )