Guide: HD Wallets & Mnemonics

This guide covers BIP39 mnemonics and BIP32 hierarchical deterministic wallets, including watch-only (xpub) derivation.

From mnemonic to addresses

from libcrypto import HDWallet, Wallet, generate_mnemonic

mnemonic = generate_mnemonic(12)
hd = HDWallet.from_mnemonic(mnemonic, passphrase="")

# BIP44 Ethereum account
eth_node = hd.derive_from_path("m/44'/60'/0'/0/0")
eth_wallet = Wallet(eth_node.private_key)
print("ETH:", eth_wallet.get_address("ethereum"))

# BIP84 native SegWit Bitcoin account
btc_node = hd.derive_from_path("m/84'/0'/0'/0/0")
btc_wallet = Wallet(btc_node.private_key)
print("BTC:", btc_wallet.get_address("bitcoin", "p2wpkh"))

Common derivation paths

Path Purpose
m/44'/0'/0'/0/0 Bitcoin legacy (BIP44).
m/49'/0'/0'/0/0 Bitcoin nested SegWit (BIP49).
m/84'/0'/0'/0/0 Bitcoin native SegWit (BIP84).
m/86'/0'/0'/0/0 Bitcoin Taproot (BIP86).
m/44'/60'/0'/0/0 Ethereum / EVM.
m/44'/195'/0'/0/0 TRON.

Extended keys (xprv / xpub)

from libcrypto import HDWallet, HDNode

hd = HDWallet(bytes.fromhex("000102030405060708090a0b0c0d0e0f"))
account = hd.derive_from_path("m/0'")

xprv = account.serialize_private()
xpub = account.serialize_public()

# Round-trip
restored_priv = HDNode.deserialize(xprv)
restored_pub = HDNode.deserialize(xpub)
assert restored_pub.is_private is False

Watch-only wallets (CKDpub)

A watch-only wallet can derive receiving addresses without ever holding a private key. neuter() produces a public-only node, and non-hardened children are derived through elliptic-curve point addition (CKDpub):

from libcrypto import HDWallet, PublicKey

hd = HDWallet(bytes.fromhex("000102030405060708090a0b0c0d0e0f"))
account = hd.derive_from_path("m/44'/0'/0'")   # has a private key

# Export only the public account node to an online system.
xpub = account.neuter().serialize_public()

# On the online system, import the xpub and derive receive addresses.
from libcrypto import HDNode
watch = HDNode.deserialize(xpub)
for i in range(3):
    child = watch.derive_child(0).derive_child(i)   # m/.../0/i
    address = PublicKey(child.public_key).get_address("p2pkh", "bitcoin")
    print(i, address)

Public derivation reproduces exactly the same public key (and therefore the same address) that private derivation would produce. Hardened derivation from a watch-only node is impossible by design and raises BIP32Error.

Security reminders

  • Treat mnemonics, seeds, and xprv strings as top-secret material.
  • Only ever export xpub (never xprv) to online or third-party systems.
  • Remember that leaking an xpub plus any single child private key can expose the parent private key for non-hardened branches — keep account roots hardened.