Skip to main content

Try it Live

Run HDWallet examples in the interactive playground

Overview

Derivation paths define hierarchical routes from master seed to specific keys. BIP-32 defines the structure, BIP-44 standardizes multi-account usage, and SLIP-44 assigns coin types.
Examples:

BIP-32 Path Format

Standard Notation

m / purpose' / coin_type' / account' / change / address_index
Components:
  • m: Master key (root)
  • purpose': Use case (44’ = BIP-44, 49’ = SegWit, 84’ = Native SegWit)
  • coin_type': Cryptocurrency (0’ = Bitcoin, 60’ = Ethereum)
  • account': Account number (0’, 1’, 2’, …)
  • change: External (0) or internal/change (1)
  • address_index: Address within account (0, 1, 2, …)
Hardened Notation:
  • ' (apostrophe): Hardened derivation
  • h: Alternative hardened notation (m/44h/60h/0h)

Path Examples

import * as HDWallet from '@tevm/voltaire/HDWallet';

// Ethereum standard (BIP-44)
const eth = "m/44'/60'/0'/0/0";
//  │   │   │   │   │  └─ First address
//  │   │   │   │   └──── External chain (receiving)
//  │   │   │   └──────── First account
//  │   │   └──────────── Ethereum
//  │   └──────────────── BIP-44
//  └──────────────────── Master

// Bitcoin standard
const btc = "m/44'/0'/0'/0/0";
//              │   │
//              │   └──── First account
//              └──────── Bitcoin

// Ethereum second account
const eth2 = "m/44'/60'/1'/0/0";
//                    │
//                    └──── Second account

BIP-44 Standard

Hierarchy Levels

Level 1 - Purpose Fixed at 44’ for BIP-44:
// Always use 44' for BIP-44
const purpose = 44 | HDWallet.HARDENED_OFFSET; // 0x80000000 + 44
Level 2 - Coin Type (SLIP-44)
// Common coin types
const coinTypes = {
  Bitcoin: 0,
  Testnet: 1,
  Ethereum: 60,
  EthereumClassic: 61,
  Litecoin: 2,
  Dogecoin: 3,
};

// Ethereum path
const ethPath = `m/44'/${coinTypes.Ethereum}'/0'/0/0`;
Level 3 - Account
// Multiple accounts for organization
const account0 = "m/44'/60'/0'/0/0"; // Personal
const account1 = "m/44'/60'/1'/0/0"; // Business
const account2 = "m/44'/60'/2'/0/0"; // Trading
Level 4 - Change
// External (receiving addresses)
const external = "m/44'/60'/0'/0/0";
//                           │
//                           └─ 0 = External

// Internal (change addresses, less common in Ethereum)
const internal = "m/44'/60'/0'/1/0";
//                           │
//                           └─ 1 = Internal
Level 5 - Address Index
// Sequential addresses
const addresses = [
  "m/44'/60'/0'/0/0",
  "m/44'/60'/0'/0/1",
  "m/44'/60'/0'/0/2",
  "m/44'/60'/0'/0/3",
  "m/44'/60'/0'/0/4",
];

Ethereum Derivation

import * as Bip39 from '@tevm/voltaire/Bip39';
import * as HDWallet from '@tevm/voltaire/HDWallet';

const mnemonic = Bip39.generateMnemonic(256);
const seed = await Bip39.mnemonicToSeed(mnemonic);
const root = HDWallet.fromSeed(seed);

// Derive using full path
const eth0 = HDWallet.derivePath(root, "m/44'/60'/0'/0/0");

// Or use convenience method
const eth0Alt = HDWallet.deriveEthereum(root, 0, 0);
// Equivalent to m/44'/60'/0'/0/0

// Multiple addresses
const eth1 = HDWallet.deriveEthereum(root, 0, 1); // m/44'/60'/0'/0/1
const eth2 = HDWallet.deriveEthereum(root, 0, 2); // m/44'/60'/0'/0/2

Bitcoin Derivation

// Bitcoin uses change addresses more commonly
const btc0 = HDWallet.derivePath(root, "m/44'/0'/0'/0/0"); // Receiving
const btcChange = HDWallet.derivePath(root, "m/44'/0'/0'/1/0"); // Change

// Or use convenience method
const btc0Alt = HDWallet.deriveBitcoin(root, 0, 0);

Hardened vs Normal Derivation

Hardened Derivation

Index ≥ 2^31 (0x80000000):
// Hardened indices
const hardened = [
  HDWallet.HARDENED_OFFSET + 0,  // 2147483648
  HDWallet.HARDENED_OFFSET + 1,  // 2147483649
  HDWallet.HARDENED_OFFSET + 44, // 2147483692 (for BIP-44)
];

// Notation in path
const hardenedPath = "m/44'/60'/0'"; // 44', 60', 0' all hardened

Normal Derivation

Index < 2^31:
// Normal indices
const normal = [0, 1, 2, 3, 4, /* ... */, 2147483647];

// Notation in path
const normalPath = "m/44'/60'/0'/0/0"; // Last two (0, 0) are normal

Security Implications

Hardened Derivation:
  • Requires private key
  • More secure (leaked child key doesn’t compromise parent)
  • Used for: purpose, coin_type, account
Normal Derivation:
  • Can derive from public key only
  • Allows watch-only wallets
  • Used for: change, address_index
// xpub can derive normal children only
const xpub = root.toExtendedPublicKey();
const pubOnly = HDWallet.fromPublicExtendedKey(xpub);

// ✅ Can derive normal children
const child0 = HDWallet.deriveChild(pubOnly, 0); // Works

// ❌ Cannot derive hardened children
try {
  HDWallet.deriveChild(pubOnly, HDWallet.HARDENED_OFFSET);
} catch (error) {
  console.error('Cannot derive hardened from public key');
}

Common Derivation Paths

Ethereum Wallets

MetaMask / Standard:
m/44'/60'/0'/0/0   First account
m/44'/60'/0'/0/1   Second address
m/44'/60'/0'/0/2   Third address
Ledger Live:
m/44'/60'/0'/0/0   Default
m/44'/60'/1'/0/0   Second account
m/44'/60'/2'/0/0   Third account
MyEtherWallet (Legacy):
m/44'/60'/0'/0     No final index (deprecated)

Multi-Coin Wallets

const paths = {
  // Ethereum
  ETH: "m/44'/60'/0'/0/0",

  // Bitcoin
  BTC: "m/44'/0'/0'/0/0",

  // Litecoin
  LTC: "m/44'/2'/0'/0/0",

  // Dogecoin
  DOGE: "m/44'/3'/0'/0/0",

  // Ethereum Classic
  ETC: "m/44'/61'/0'/0/0",
};

// Derive all from same seed
for (const [coin, path] of Object.entries(paths)) {
  const key = HDWallet.derivePath(root, path);
  console.log(`${coin}:`, key.getPublicKey());
}

Path Parsing and Validation

Validate Path Format

import * as HDWallet from '@tevm/voltaire/HDWallet';

// Valid paths
const valid = [
  "m/44'/60'/0'/0/0",
  "m/0",
  "m/0'/1'/2'",
  "m/44h/60h/0h/0/0", // h notation
];

valid.forEach(path => {
  console.log(path, HDWallet.isValidPath(path)); // All true
});

// Invalid paths
const invalid = [
  "44'/60'/0'/0/0",    // Missing 'm'
  "m//44'/60'/0'",     // Empty level
  "m/44'/60'/0'/0/",   // Trailing slash
  "m/invalid",         // Non-numeric
];

invalid.forEach(path => {
  console.log(path, HDWallet.isValidPath(path)); // All false
});

Parse Index

// Parse index strings
console.log(HDWallet.parseIndex("0"));   // 0
console.log(HDWallet.parseIndex("44"));  // 44
console.log(HDWallet.parseIndex("0'"));  // 2147483648 (HARDENED_OFFSET)
console.log(HDWallet.parseIndex("60h")); // 2147483708 (HARDENED_OFFSET + 60)

Check Hardened Path

const paths = [
  "m/44'/60'/0'/0/0", // Has hardened
  "m/0/1/2",          // No hardened
  "m/0'/1/2",         // Mixed
];

paths.forEach(path => {
  console.log(path, HDWallet.isHardenedPath(path));
});
// true, false, true

Advanced Path Patterns

Gap Limit (BIP-44)

Scan for used addresses with gap limit:
async function scanAddresses(root: ExtendedKey, gapLimit = 20): Promise<string[]> {
  const usedAddresses = [];
  let consecutiveUnused = 0;

  for (let i = 0; ; i++) {
    const key = HDWallet.deriveEthereum(root, 0, i);
    const address = deriveAddress(key);

    const hasTransactions = await checkAddressUsed(address);

    if (hasTransactions) {
      usedAddresses.push(address);
      consecutiveUnused = 0;
    } else {
      consecutiveUnused++;

      if (consecutiveUnused >= gapLimit) {
        break; // Stop scanning
      }
    }
  }

  return usedAddresses;
}

Custom Derivation

// Custom path for specific use case
const customPaths = {
  // Hot wallet
  hot: "m/44'/60'/0'/0/0",

  // Cold storage
  cold: "m/44'/60'/1'/0/0",

  // DeFi interactions
  defi: "m/44'/60'/2'/0/0",

  // NFT trading
  nft: "m/44'/60'/3'/0/0",
};

for (const [purpose, path] of Object.entries(customPaths)) {
  const key = HDWallet.derivePath(root, path);
  console.log(`${purpose}:`, deriveAddress(key));
}

Deterministic Per-Service Accounts

// Derive unique account per service
function deriveServiceAccount(root: ExtendedKey, serviceName: string): ExtendedKey {
  // Hash service name to deterministic account index
  const hash = sha256(serviceName);
  const accountIndex = new DataView(hash.buffer).getUint32(0, false);

  // Use as account index (ensure < 2^31 for non-hardened)
  const index = accountIndex % (HDWallet.HARDENED_OFFSET);

  return HDWallet.deriveEthereum(root, index, 0);
}

const uniswapAccount = deriveServiceAccount(root, 'uniswap');
const aaveAccount = deriveServiceAccount(root, 'aave');

Path Constants

// Pre-defined path templates
const BIP44_PATHS = {
  ETH: (account = 0, index = 0) => `m/44'/60'/${account}'/0/${index}`,
  BTC: (account = 0, index = 0) => `m/44'/0'/${account}'/0/${index}`,
  LTC: (account = 0, index = 0) => `m/44'/2'/${account}'/0/${index}`,
};

// Usage
const eth0 = HDWallet.derivePath(root, BIP44_PATHS.ETH(0, 0));
const btc5 = HDWallet.derivePath(root, BIP44_PATHS.BTC(0, 5));

Best Practices

1. Use Standard Paths
// ✅ Standard BIP-44
const standard = "m/44'/60'/0'/0/0";

// ❌ Non-standard (reduces compatibility)
const nonStandard = "m/0/0/0/0/0";
2. Harden Sensitive Levels
// ✅ Hardened: purpose, coin_type, account
const secure = "m/44'/60'/0'/0/0";
//                ^^^ ^^^ ^^^ - Hardened

// ❌ Non-hardened sensitive levels
const insecure = "m/44/60/0/0/0";
3. Document Custom Paths
interface WalletConfig {
  derivationPath: string;
  purpose: string;
  notes?: string;
}

const config: WalletConfig = {
  derivationPath: "m/44'/60'/0'/0/0",
  purpose: 'Standard Ethereum wallet',
  notes: 'Compatible with MetaMask'
};
4. Validate Before Derivation
function safeDervePath(root: ExtendedKey, path: string): ExtendedKey {
  if (!HDWallet.isValidPath(path)) {
    throw new Error(`Invalid derivation path: ${path}`);
  }

  return HDWallet.derivePath(root, path);
}

References