Module jmwallet.wallet.bip32

BIP32 HD key derivation for JoinMarket wallets. Implements BIP84 (Native SegWit) derivation paths.

Functions

def mnemonic_to_seed(mnemonic: str, passphrase: str = '') ‑> bytes
Expand source code
def mnemonic_to_seed(mnemonic: str, passphrase: str = "") -> bytes:
    """
    Convert BIP39 mnemonic to seed.
    Simple implementation - for production use python-mnemonic library.
    """
    from hashlib import pbkdf2_hmac

    mnemonic_bytes = mnemonic.encode("utf-8")
    salt = ("mnemonic" + passphrase).encode("utf-8")

    seed = pbkdf2_hmac("sha512", mnemonic_bytes, salt, 2048, dklen=64)
    return seed

Convert BIP39 mnemonic to seed. Simple implementation - for production use python-mnemonic library.

Classes

class HDKey (private_key: PrivateKey,
chain_code: bytes,
depth: int = 0,
parent_fingerprint: bytes = b'\x00\x00\x00\x00',
child_number: int = 0)
Expand source code
class HDKey:
    """
    Hierarchical Deterministic Key for Bitcoin.
    Implements BIP32 derivation.
    """

    def __init__(
        self,
        private_key: PrivateKey,
        chain_code: bytes,
        depth: int = 0,
        parent_fingerprint: bytes = b"\x00\x00\x00\x00",
        child_number: int = 0,
    ):
        self._private_key = private_key
        self._public_key = private_key.public_key
        self.chain_code = chain_code
        self.depth = depth
        self.parent_fingerprint = parent_fingerprint
        self.child_number = child_number

    @property
    def private_key(self) -> PrivateKey:
        """Return the coincurve PrivateKey instance."""
        return self._private_key

    @property
    def public_key(self) -> PublicKey:
        """Return the coincurve PublicKey instance."""
        return self._public_key

    @property
    def fingerprint(self) -> bytes:
        """Get the fingerprint of this key (first 4 bytes of hash160 of public key)."""
        pubkey_bytes = self._public_key.format(compressed=True)
        sha256_hash = hashlib.sha256(pubkey_bytes).digest()
        ripemd160_hash = hashlib.new("ripemd160", sha256_hash).digest()
        return ripemd160_hash[:4]

    @classmethod
    def from_seed(cls, seed: bytes) -> HDKey:
        """Create master HD key from seed"""
        hmac_result = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
        key_bytes = hmac_result[:32]
        chain_code = hmac_result[32:]

        private_key = PrivateKey(key_bytes)

        return cls(private_key, chain_code, depth=0)

    def derive(self, path: str) -> HDKey:
        """
        Derive child key from path notation (e.g., "m/84'/0'/0'/0/0")
        ' indicates hardened derivation
        """
        if not path.startswith("m"):
            raise ValueError("Path must start with 'm'")

        parts = path.split("/")[1:]
        key = self

        for part in parts:
            if not part:
                continue

            hardened = part.endswith("'") or part.endswith("h")
            index_str = part.rstrip("'h")
            index = int(index_str)

            if hardened:
                index += 0x80000000

            key = key._derive_child(index)

        return key

    def _derive_child(self, index: int) -> HDKey:
        """Derive a child key at the given index"""
        hardened = index >= 0x80000000

        if hardened:
            priv_bytes = self._private_key.secret
            data = b"\x00" + priv_bytes + index.to_bytes(4, "big")
        else:
            pub_bytes = self._public_key.format(compressed=True)
            data = pub_bytes + index.to_bytes(4, "big")

        hmac_result = hmac.new(self.chain_code, data, hashlib.sha512).digest()
        key_offset = hmac_result[:32]
        child_chain = hmac_result[32:]

        parent_key_int = int.from_bytes(self._private_key.secret, "big")
        offset_int = int.from_bytes(key_offset, "big")

        child_key_int = (parent_key_int + offset_int) % SECP256K1_N

        if child_key_int == 0:
            raise ValueError("Invalid child key")

        child_key_bytes = child_key_int.to_bytes(32, "big")
        child_private_key = PrivateKey(child_key_bytes)

        return HDKey(
            child_private_key,
            child_chain,
            depth=self.depth + 1,
            parent_fingerprint=self.fingerprint,
            child_number=index,
        )

    def get_private_key_bytes(self) -> bytes:
        """Get private key as 32 bytes"""
        return self._private_key.secret

    def get_public_key_bytes(self, compressed: bool = True) -> bytes:
        """Get public key bytes"""
        return self._public_key.format(compressed=compressed)

    def get_address(self, network: str = "mainnet") -> str:
        """Get P2WPKH (Native SegWit) address for this key"""
        from jmwallet.wallet.address import pubkey_to_p2wpkh_address

        pubkey_hex = self.get_public_key_bytes(compressed=True).hex()
        return pubkey_to_p2wpkh_address(pubkey_hex, network)

    def sign(self, message: bytes) -> bytes:
        """Sign a message with this key (uses SHA256 hashing)."""
        return self._private_key.sign(message)

    def get_xpub(self, network: str = "mainnet") -> str:
        """
        Serialize the public key as an extended public key (xpub/tpub).

        This produces a standard BIP32 xpub that can be used in Bitcoin Core
        descriptors. The descriptor wrapper (wpkh, wsh, etc.) determines the
        actual address type.

        Args:
            network: "mainnet" for xpub, "testnet"/"regtest" for tpub

        Returns:
            Base58Check-encoded extended public key (xpub or tpub)
        """
        if network == "mainnet":
            version = XPUB_MAINNET
        else:
            version = XPUB_TESTNET

        # BIP32 serialization format:
        # 4 bytes: version
        # 1 byte: depth
        # 4 bytes: parent fingerprint
        # 4 bytes: child number
        # 32 bytes: chain code
        # 33 bytes: public key (compressed)
        depth_byte = min(self.depth, 255).to_bytes(1, "big")
        child_num_bytes = self.child_number.to_bytes(4, "big")
        pubkey_bytes = self._public_key.format(compressed=True)

        payload = (
            version
            + depth_byte
            + self.parent_fingerprint
            + child_num_bytes
            + self.chain_code
            + pubkey_bytes
        )

        return _base58check_encode(payload)

    def get_xprv(self, network: str = "mainnet") -> str:
        """
        Serialize the private key as an extended private key (xprv/tprv).

        Args:
            network: "mainnet" for xprv, "testnet"/"regtest" for tprv

        Returns:
            Base58Check-encoded extended private key
        """
        if network == "mainnet":
            version = XPRV_MAINNET
        else:
            version = XPRV_TESTNET

        depth_byte = min(self.depth, 255).to_bytes(1, "big")
        child_num_bytes = self.child_number.to_bytes(4, "big")
        # Private key is prefixed with 0x00 to make it 33 bytes
        privkey_bytes = b"\x00" + self._private_key.secret

        payload = (
            version
            + depth_byte
            + self.parent_fingerprint
            + child_num_bytes
            + self.chain_code
            + privkey_bytes
        )

        return _base58check_encode(payload)

Hierarchical Deterministic Key for Bitcoin. Implements BIP32 derivation.

Static methods

def from_seed(seed: bytes) ‑> HDKey

Create master HD key from seed

Instance variables

prop fingerprint : bytes
Expand source code
@property
def fingerprint(self) -> bytes:
    """Get the fingerprint of this key (first 4 bytes of hash160 of public key)."""
    pubkey_bytes = self._public_key.format(compressed=True)
    sha256_hash = hashlib.sha256(pubkey_bytes).digest()
    ripemd160_hash = hashlib.new("ripemd160", sha256_hash).digest()
    return ripemd160_hash[:4]

Get the fingerprint of this key (first 4 bytes of hash160 of public key).

prop private_key : PrivateKey
Expand source code
@property
def private_key(self) -> PrivateKey:
    """Return the coincurve PrivateKey instance."""
    return self._private_key

Return the coincurve PrivateKey instance.

prop public_key : PublicKey
Expand source code
@property
def public_key(self) -> PublicKey:
    """Return the coincurve PublicKey instance."""
    return self._public_key

Return the coincurve PublicKey instance.

Methods

def derive(self, path: str) ‑> HDKey
Expand source code
def derive(self, path: str) -> HDKey:
    """
    Derive child key from path notation (e.g., "m/84'/0'/0'/0/0")
    ' indicates hardened derivation
    """
    if not path.startswith("m"):
        raise ValueError("Path must start with 'm'")

    parts = path.split("/")[1:]
    key = self

    for part in parts:
        if not part:
            continue

        hardened = part.endswith("'") or part.endswith("h")
        index_str = part.rstrip("'h")
        index = int(index_str)

        if hardened:
            index += 0x80000000

        key = key._derive_child(index)

    return key

Derive child key from path notation (e.g., "m/84'/0'/0'/0/0") ' indicates hardened derivation

def get_address(self, network: str = 'mainnet') ‑> str
Expand source code
def get_address(self, network: str = "mainnet") -> str:
    """Get P2WPKH (Native SegWit) address for this key"""
    from jmwallet.wallet.address import pubkey_to_p2wpkh_address

    pubkey_hex = self.get_public_key_bytes(compressed=True).hex()
    return pubkey_to_p2wpkh_address(pubkey_hex, network)

Get P2WPKH (Native SegWit) address for this key

def get_private_key_bytes(self) ‑> bytes
Expand source code
def get_private_key_bytes(self) -> bytes:
    """Get private key as 32 bytes"""
    return self._private_key.secret

Get private key as 32 bytes

def get_public_key_bytes(self, compressed: bool = True) ‑> bytes
Expand source code
def get_public_key_bytes(self, compressed: bool = True) -> bytes:
    """Get public key bytes"""
    return self._public_key.format(compressed=compressed)

Get public key bytes

def get_xprv(self, network: str = 'mainnet') ‑> str
Expand source code
def get_xprv(self, network: str = "mainnet") -> str:
    """
    Serialize the private key as an extended private key (xprv/tprv).

    Args:
        network: "mainnet" for xprv, "testnet"/"regtest" for tprv

    Returns:
        Base58Check-encoded extended private key
    """
    if network == "mainnet":
        version = XPRV_MAINNET
    else:
        version = XPRV_TESTNET

    depth_byte = min(self.depth, 255).to_bytes(1, "big")
    child_num_bytes = self.child_number.to_bytes(4, "big")
    # Private key is prefixed with 0x00 to make it 33 bytes
    privkey_bytes = b"\x00" + self._private_key.secret

    payload = (
        version
        + depth_byte
        + self.parent_fingerprint
        + child_num_bytes
        + self.chain_code
        + privkey_bytes
    )

    return _base58check_encode(payload)

Serialize the private key as an extended private key (xprv/tprv).

Args

network
"mainnet" for xprv, "testnet"/"regtest" for tprv

Returns

Base58Check-encoded extended private key

def get_xpub(self, network: str = 'mainnet') ‑> str
Expand source code
def get_xpub(self, network: str = "mainnet") -> str:
    """
    Serialize the public key as an extended public key (xpub/tpub).

    This produces a standard BIP32 xpub that can be used in Bitcoin Core
    descriptors. The descriptor wrapper (wpkh, wsh, etc.) determines the
    actual address type.

    Args:
        network: "mainnet" for xpub, "testnet"/"regtest" for tpub

    Returns:
        Base58Check-encoded extended public key (xpub or tpub)
    """
    if network == "mainnet":
        version = XPUB_MAINNET
    else:
        version = XPUB_TESTNET

    # BIP32 serialization format:
    # 4 bytes: version
    # 1 byte: depth
    # 4 bytes: parent fingerprint
    # 4 bytes: child number
    # 32 bytes: chain code
    # 33 bytes: public key (compressed)
    depth_byte = min(self.depth, 255).to_bytes(1, "big")
    child_num_bytes = self.child_number.to_bytes(4, "big")
    pubkey_bytes = self._public_key.format(compressed=True)

    payload = (
        version
        + depth_byte
        + self.parent_fingerprint
        + child_num_bytes
        + self.chain_code
        + pubkey_bytes
    )

    return _base58check_encode(payload)

Serialize the public key as an extended public key (xpub/tpub).

This produces a standard BIP32 xpub that can be used in Bitcoin Core descriptors. The descriptor wrapper (wpkh, wsh, etc.) determines the actual address type.

Args

network
"mainnet" for xpub, "testnet"/"regtest" for tpub

Returns

Base58Check-encoded extended public key (xpub or tpub)

def sign(self, message: bytes) ‑> bytes
Expand source code
def sign(self, message: bytes) -> bytes:
    """Sign a message with this key (uses SHA256 hashing)."""
    return self._private_key.sign(message)

Sign a message with this key (uses SHA256 hashing).