Skip to main content

Try it Live

Run HDWallet examples in the interactive playground

Overview

HD wallets derive child keys from parent keys using HMAC-SHA512. BIP-32 defines two derivation methods: hardened (more secure) and normal (allows public derivation).
Examples:

Derivation Algorithm

Parent → Child Process

Step 1: Prepare data
  - Normal: data = parent_public_key || index (33 + 4 bytes)
  - Hardened: data = 0x00 || parent_private_key || index (1 + 32 + 4 bytes)

Step 2: HMAC-SHA512
  I = HMAC-SHA512(parent_chain_code, data)

Step 3: Split result
  IL = I[0:32]   (left 32 bytes = key material)
  IR = I[32:64]  (right 32 bytes = new chain code)

Step 4: Compute child key
  - Private: child_key = (IL + parent_key) mod n
  - Public: child_pubkey = IL*G + parent_pubkey

Step 5: Result
  child_private_key = child_key
  child_public_key = child_pubkey
  child_chain_code = IR

Hardened Derivation

Index Range

Hardened indices: 2^31 to 2^32 - 1 (2147483648 to 4294967295)
import * as HDWallet from '@tevm/voltaire/HDWallet';

// Hardened offset
const HARDENED = HDWallet.HARDENED_OFFSET; // 0x80000000 = 2147483648

// Hardened indices
const hardenedIndices = [
  HARDENED + 0,  // 2147483648 (0')
  HARDENED + 1,  // 2147483649 (1')
  HARDENED + 44, // 2147483692 (44')
  HARDENED + 60, // 2147483708 (60')
];

// Derive hardened children
const child0h = HDWallet.deriveChild(root, HARDENED + 0);
const child1h = HDWallet.deriveChild(root, HARDENED + 1);

Notation

// Two equivalent notations
const apostrophe = "m/0'/1'/2'";    // Single quote (standard)
const h_notation = "m/0h/1h/2h";    // 'h' suffix

// Both derive same keys
const key1 = HDWallet.derivePath(root, apostrophe);
const key2 = HDWallet.derivePath(root, h_notation);

const priv1 = key1.getPrivateKey();
const priv2 = key2.getPrivateKey();

console.log(priv1.every((b, i) => b === priv2![i])); // true

Security Properties

Requires Private Key:
// ✅ Can derive hardened from private key
const xprv = root.toExtendedPrivateKey();
const key = HDWallet.fromExtendedKey(xprv);
const hardened = HDWallet.deriveChild(key, HARDENED + 0); // Works

// ❌ Cannot derive hardened from public key
const xpub = root.toExtendedPublicKey();
const pubOnly = HDWallet.fromPublicExtendedKey(xpub);

try {
  HDWallet.deriveChild(pubOnly, HARDENED + 0);
} catch (error) {
  console.error('Cannot derive hardened from public key');
}
Leak Protection:
/**
 * If attacker obtains:
 * 1. Parent xpub
 * 2. Any non-hardened child private key
 *
 * They can compute all sibling private keys!
 *
 * Hardened derivation prevents this:
 * - Requires parent private key
 * - Leaked child + parent xpub doesn't compromise siblings
 */

// Example vulnerability (NON-hardened)
const parent = HDWallet.derivePath(root, "m/44'/60'/0'");
const parentXpub = parent.toExtendedPublicKey();

// Derive non-hardened children
const child0 = HDWallet.derivePath(parent, "m/0/0");
const child1 = HDWallet.derivePath(parent, "m/0/1");

// If child0 private key + parentXpub leaked:
// Attacker can compute child1, child2, ... childN private keys

// Protection: Use hardened derivation
const secureChild0 = HDWallet.derivePath(parent, "m/0'/0");
const secureChild1 = HDWallet.derivePath(parent, "m/0'/1");
// Now leak-resistant

Normal Derivation

Index Range

Normal indices: 0 to 2^31 - 1 (0 to 2147483647)
// Normal indices
const normalIndices = [0, 1, 2, 3, 4, /* ... */, 2147483647];

// Derive normal children
const child0 = HDWallet.deriveChild(root, 0);
const child1 = HDWallet.deriveChild(root, 1);
const child2 = HDWallet.deriveChild(root, 2);

Public Derivation

Normal derivation allows deriving children from parent public key:
// From parent xpub
const xpub = root.toExtendedPublicKey();
const pubOnly = HDWallet.fromPublicExtendedKey(xpub);

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

// Get public keys (no private keys)
const pubKey0 = child0.getPublicKey();
const pubKey1 = child1.getPublicKey();

console.log(pubKey0); // Uint8Array(33)
console.log(child0.getPrivateKey()); // null

Use Cases

1. Watch-Only Wallets:
// Server monitors addresses without private keys
const accountXpub = await getXpubFromConfig();
const watchOnly = HDWallet.fromPublicExtendedKey(accountXpub);

// Generate receiving addresses
for (let i = 0; i < 100; i++) {
  const child = HDWallet.deriveChild(watchOnly, i);
  const address = deriveAddress(child);
  await monitorAddress(address);
}
2. Server-Side Address Generation:
// E-commerce platform generates unique address per order
async function generatePaymentAddress(orderId: string): Promise<string> {
  const xpub = await getShopXpub();
  const watchOnly = HDWallet.fromPublicExtendedKey(xpub);

  // Use order ID as deterministic index
  const index = hashToIndex(orderId);

  const child = HDWallet.deriveChild(watchOnly, index);
  return deriveAddress(child);
}
3. Auditing:
// Auditor can view all addresses without spending ability
const auditXpub = '...'; // Provided by wallet owner
const auditor = HDWallet.fromPublicExtendedKey(auditXpub);

// Generate all addresses for audit
const addresses = [];
for (let i = 0; i < 1000; i++) {
  const child = HDWallet.deriveChild(auditor, i);
  addresses.push(deriveAddress(child));
}

// Audit transaction history
await auditTransactions(addresses);

Sequential Derivation

Single-Level Derivation

// Derive one level at a time
const level1 = HDWallet.deriveChild(root, HARDENED + 44); // m/44'
const level2 = HDWallet.deriveChild(level1, HARDENED + 60); // m/44'/60'
const level3 = HDWallet.deriveChild(level2, HARDENED + 0); // m/44'/60'/0'
const level4 = HDWallet.deriveChild(level3, 0); // m/44'/60'/0'/0
const level5 = HDWallet.deriveChild(level4, 0); // m/44'/60'/0'/0/0

// Equivalent to
const direct = HDWallet.derivePath(root, "m/44'/60'/0'/0/0");

// Verify equivalence
const seq = level5.getPrivateKey();
const dir = direct.getPrivateKey();
console.log(seq!.every((b, i) => b === dir![i])); // true

Multi-Account Derivation

// Derive multiple accounts efficiently
const ethCoinType = HDWallet.derivePath(root, "m/44'/60'");

// Derive accounts from coin-type level
const account0 = HDWallet.deriveChild(ethCoinType, HARDENED + 0);
const account1 = HDWallet.deriveChild(ethCoinType, HARDENED + 1);
const account2 = HDWallet.deriveChild(ethCoinType, HARDENED + 2);

// Each account can derive many addresses
for (let i = 0; i < 10; i++) {
  const change = HDWallet.deriveChild(account0, 0);
  const addr = HDWallet.deriveChild(change, i);
  console.log(`Account 0, Address ${i}:`, deriveAddress(addr));
}

Batch Derivation

// Derive many addresses efficiently
function deriveAddressRange(
  parent: ExtendedKey,
  start: number,
  count: number
): string[] {
  const addresses = [];

  for (let i = start; i < start + count; i++) {
    const child = HDWallet.deriveChild(parent, i);
    const address = deriveAddress(child);
    addresses.push(address);
  }

  return addresses;
}

// Usage
const accountLevel = HDWallet.derivePath(root, "m/44'/60'/0'/0");
const first100 = deriveAddressRange(accountLevel, 0, 100);
console.log(`Derived ${first100.length} addresses`);

Path-Based Derivation

Full Path

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

// Internally calls deriveChild multiple times:
// root → m/44' → m/44'/60' → m/44'/60'/0' → m/44'/60'/0'/0 → m/44'/60'/0'/0/5

Relative Path

// Start from intermediate level
const accountLevel = HDWallet.derivePath(root, "m/44'/60'/0'");

// Derive relative to account level
const address0 = HDWallet.derivePath(accountLevel, "m/0/0");
const address1 = HDWallet.derivePath(accountLevel, "m/0/1");

// Note: Paths are still absolute (start with 'm')
// Library handles relative derivation internally

Deterministic Derivation

Same Input = Same Output

// Deterministic property
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const seed = await Bip39.mnemonicToSeed(mnemonic);

// Derive key multiple times
const key1 = HDWallet.fromSeed(seed);
const key2 = HDWallet.fromSeed(seed);

const child1 = HDWallet.deriveChild(key1, 0);
const child2 = HDWallet.deriveChild(key2, 0);

// Always produces same result
const priv1 = child1.getPrivateKey();
const priv2 = child2.getPrivateKey();

console.log(priv1!.every((b, i) => b === priv2![i])); // true

Reproducible Wallets

// Wallet recovery reproduces exact same keys
async function testWalletRecovery() {
  // Original wallet
  const originalMnemonic = Bip39.generateMnemonic(256);
  const originalSeed = await Bip39.mnemonicToSeed(originalMnemonic);
  const originalRoot = HDWallet.fromSeed(originalSeed);
  const originalAddress = deriveAddress(
    HDWallet.deriveEthereum(originalRoot, 0, 0)
  );

  // Simulate recovery
  const recoveredSeed = await Bip39.mnemonicToSeed(originalMnemonic);
  const recoveredRoot = HDWallet.fromSeed(recoveredSeed);
  const recoveredAddress = deriveAddress(
    HDWallet.deriveEthereum(recoveredRoot, 0, 0)
  );

  // Verify exact match
  console.log('Match:', originalAddress === recoveredAddress); // true
}

Chain Code Usage

Chain Code in Derivation

/**
 * Chain code (32 bytes):
 * - Used as HMAC key for child derivation
 * - Different from private key
 * - Included in extended keys
 * - Essential for deterministic derivation
 */

const chainCode = root.getChainCode();
console.log(chainCode); // Uint8Array(32)

// Child derivation uses parent's chain code
// I = HMAC-SHA512(parent_chain_code, data)

Chain Code Secrecy

/**
 * Chain code + public key = ability to derive all normal children
 *
 * If attacker gets:
 * 1. Parent chain code
 * 2. Parent public key
 * 3. Any child private key (normal)
 *
 * They can compute all sibling private keys!
 */

// xpub includes chain code
const xpub = root.toExtendedPublicKey();
// Contains: public_key + chain_code + metadata

// Safe to share xpub (designed for this)
// But understand it reveals address structure

Advanced Derivation Patterns

Gap Limit Scanning

// BIP-44 gap limit = 20
async function scanForUsedAddresses(root: ExtendedKey): Promise<string[]> {
  const GAP_LIMIT = 20;
  const usedAddresses = [];
  let consecutiveUnused = 0;

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

    const hasTransactions = await checkAddressHasTransactions(address);

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

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

  return usedAddresses;
}

Parallel Derivation

// Derive multiple children in parallel
async function deriveParallel(
  root: ExtendedKey,
  indices: number[]
): Promise<ExtendedKey[]> {
  return Promise.all(
    indices.map(i => Promise.resolve(HDWallet.deriveChild(root, i)))
  );
}

// Usage
const indices = [0, 1, 2, 3, 4];
const children = await deriveParallel(root, indices);
console.log(`Derived ${children.length} children`);

Cached Derivation

// Cache frequently-used derivation paths
class DerivationCache {
  private cache = new Map<string, ExtendedKey>();

  derive(root: ExtendedKey, path: string): ExtendedKey {
    if (this.cache.has(path)) {
      return this.cache.get(path)!;
    }

    const key = HDWallet.derivePath(root, path);
    this.cache.set(path, key);
    return key;
  }

  clear() {
    this.cache.clear();
  }
}

// Usage
const cache = new DerivationCache();
const key1 = cache.derive(root, "m/44'/60'/0'/0/0"); // Derives
const key2 = cache.derive(root, "m/44'/60'/0'/0/0"); // Cached

Error Handling

Invalid Index

// Index must be 0 to 2^32-1
try {
  HDWallet.deriveChild(root, -1); // Invalid
} catch (error) {
  console.error('Invalid index');
}

try {
  HDWallet.deriveChild(root, 0x100000000); // > 2^32-1
} catch (error) {
  console.error('Index too large');
}

Hardened from Public Key

const xpub = root.toExtendedPublicKey();
const pubOnly = HDWallet.fromPublicExtendedKey(xpub);

try {
  HDWallet.deriveChild(pubOnly, HARDENED + 0);
} catch (error) {
  console.error('Cannot derive hardened from public key');
}

Invalid Path Format

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

invalidPaths.forEach(path => {
  try {
    HDWallet.derivePath(root, path);
  } catch (error) {
    console.error(`Invalid path: ${path}`);
  }
});

Best Practices

1. Use Hardened for Sensitive Levels
// ✅ BIP-44 standard (hardened purpose, coin, account)
const secure = "m/44'/60'/0'/0/0";

// ❌ Non-hardened sensitive levels
const insecure = "m/44/60/0/0/0";
2. Cache Intermediate Levels
// ✅ Efficient: Derive to account level once
const accountLevel = HDWallet.derivePath(root, "m/44'/60'/0'");

// Then derive many addresses
for (let i = 0; i < 1000; i++) {
  const child = HDWallet.deriveChild(
    HDWallet.deriveChild(accountLevel, 0),
    i
  );
}

// ❌ Inefficient: Derive full path each time
for (let i = 0; i < 1000; i++) {
  HDWallet.derivePath(root, `m/44'/60'/0'/0/${i}`);
}
3. Validate Derivation Results
function safeDeriveChild(parent: ExtendedKey, index: number): ExtendedKey {
  if (!parent.canDeriveHardened() && index >= HARDENED) {
    throw new Error('Cannot derive hardened from public key');
  }

  return HDWallet.deriveChild(parent, index);
}

References