Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

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 { HDWallet } from '@tevm/voltaire/native';

// 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