Skip to content

jmwallet.wallet.bip32

jmwallet.wallet.bip32

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

Attributes

VPRV_TESTNET = bytes.fromhex('045F18BC') module-attribute

VPUB_TESTNET = bytes.fromhex('045F1CF6') module-attribute

XPRV_MAINNET = bytes.fromhex('0488ADE4') module-attribute

XPRV_TESTNET = bytes.fromhex('04358394') module-attribute

XPUB_MAINNET = bytes.fromhex('0488B21E') module-attribute

XPUB_TESTNET = bytes.fromhex('043587CF') module-attribute

ZPRV_MAINNET = bytes.fromhex('04B2430C') module-attribute

ZPUB_MAINNET = bytes.fromhex('04B24746') module-attribute

__all__ = ['HDKey', 'mnemonic_to_seed'] module-attribute

Classes

HDKey

Hierarchical Deterministic Key for Bitcoin. Implements BIP32 derivation.

Source code in jmwallet/src/jmwallet/wallet/bip32.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
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_zpub(self, network: str = "mainnet") -> str:
        """
        Serialize the public key as a BIP84 extended public key (zpub/vpub).

        This produces a BIP84-compliant extended public key for native segwit wallets.
        zpub/vpub explicitly indicates the key is intended for P2WPKH addresses.

        Args:
            network: "mainnet" for zpub, "testnet"/"regtest" for vpub

        Returns:
            Base58Check-encoded extended public key (zpub or vpub)
        """
        if network == "mainnet":
            version = ZPUB_MAINNET
        else:
            version = VPUB_TESTNET

        # Same serialization format as xpub but with BIP84 version bytes
        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)
Attributes
chain_code = chain_code instance-attribute
child_number = child_number instance-attribute
depth = depth instance-attribute
fingerprint: bytes property

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

parent_fingerprint = parent_fingerprint instance-attribute
private_key: PrivateKey property

Return the coincurve PrivateKey instance.

public_key: PublicKey property

Return the coincurve PublicKey instance.

Functions
__init__(private_key: PrivateKey, chain_code: bytes, depth: int = 0, parent_fingerprint: bytes = b'\x00\x00\x00\x00', child_number: int = 0)
Source code in jmwallet/src/jmwallet/wallet/bip32.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
derive(path: str) -> HDKey

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

Source code in jmwallet/src/jmwallet/wallet/bip32.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
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
from_seed(seed: bytes) -> HDKey classmethod

Create master HD key from seed

Source code in jmwallet/src/jmwallet/wallet/bip32.py
71
72
73
74
75
76
77
78
79
80
@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)
get_address(network: str = 'mainnet') -> str

Get P2WPKH (Native SegWit) address for this key

Source code in jmwallet/src/jmwallet/wallet/bip32.py
150
151
152
153
154
155
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_private_key_bytes() -> bytes

Get private key as 32 bytes

Source code in jmwallet/src/jmwallet/wallet/bip32.py
142
143
144
def get_private_key_bytes(self) -> bytes:
    """Get private key as 32 bytes"""
    return self._private_key.secret
get_public_key_bytes(compressed: bool = True) -> bytes

Get public key bytes

Source code in jmwallet/src/jmwallet/wallet/bip32.py
146
147
148
def get_public_key_bytes(self, compressed: bool = True) -> bytes:
    """Get public key bytes"""
    return self._public_key.format(compressed=compressed)
get_xprv(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

Source code in jmwallet/src/jmwallet/wallet/bip32.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
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)
get_xpub(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)

Source code in jmwallet/src/jmwallet/wallet/bip32.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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)
get_zpub(network: str = 'mainnet') -> str

Serialize the public key as a BIP84 extended public key (zpub/vpub).

This produces a BIP84-compliant extended public key for native segwit wallets. zpub/vpub explicitly indicates the key is intended for P2WPKH addresses.

Args: network: "mainnet" for zpub, "testnet"/"regtest" for vpub

Returns: Base58Check-encoded extended public key (zpub or vpub)

Source code in jmwallet/src/jmwallet/wallet/bip32.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def get_zpub(self, network: str = "mainnet") -> str:
    """
    Serialize the public key as a BIP84 extended public key (zpub/vpub).

    This produces a BIP84-compliant extended public key for native segwit wallets.
    zpub/vpub explicitly indicates the key is intended for P2WPKH addresses.

    Args:
        network: "mainnet" for zpub, "testnet"/"regtest" for vpub

    Returns:
        Base58Check-encoded extended public key (zpub or vpub)
    """
    if network == "mainnet":
        version = ZPUB_MAINNET
    else:
        version = VPUB_TESTNET

    # Same serialization format as xpub but with BIP84 version bytes
    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)
sign(message: bytes) -> bytes

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

Source code in jmwallet/src/jmwallet/wallet/bip32.py
157
158
159
def sign(self, message: bytes) -> bytes:
    """Sign a message with this key (uses SHA256 hashing)."""
    return self._private_key.sign(message)

Functions

mnemonic_to_seed(mnemonic: str, passphrase: str = '') -> bytes

Convert BIP39 mnemonic to 64-byte seed using PBKDF2-HMAC-SHA512.

This follows the BIP39 specification for seed generation.

Args: mnemonic: Space-separated mnemonic words passphrase: Optional BIP39 passphrase (default empty)

Returns: 64-byte seed

Source code in jmcore/src/jmcore/crypto.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def mnemonic_to_seed(mnemonic: str, passphrase: str = "") -> bytes:
    """
    Convert BIP39 mnemonic to 64-byte seed using PBKDF2-HMAC-SHA512.

    This follows the BIP39 specification for seed generation.

    Args:
        mnemonic: Space-separated mnemonic words
        passphrase: Optional BIP39 passphrase (default empty)

    Returns:
        64-byte seed
    """
    mnemonic_bytes = mnemonic.encode("utf-8")
    salt = ("mnemonic" + passphrase).encode("utf-8")
    return hashlib.pbkdf2_hmac("sha512", mnemonic_bytes, salt, 2048, dklen=64)