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:
- Child Keys - Sequential child key derivation
- Hardened Derivation - Hardened vs non-hardened comparison
Derivation Algorithm
Parent → Child Process
Copy
Ask AI
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)
Copy
Ask AI
import * as HDWallet from '@tevm/voltaire/crypto/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
Copy
Ask AI
// 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 = HDWallet.getPrivateKey(key1);
const priv2 = HDWallet.getPrivateKey(key2);
console.log(priv1.every((b, i) => b === priv2![i])); // true
Security Properties
Requires Private Key:Copy
Ask AI
// ✅ Can derive hardened from private key
const xprv = HDWallet.toExtendedPrivateKey(root);
const key = HDWallet.fromExtendedKey(xprv);
const hardened = HDWallet.deriveChild(key, HARDENED + 0); // Works
// ❌ Cannot derive hardened from public key
const xpub = HDWallet.toExtendedPublicKey(root);
const pubOnly = HDWallet.fromPublicExtendedKey(xpub);
try {
HDWallet.deriveChild(pubOnly, HARDENED + 0);
} catch (error) {
console.error('Cannot derive hardened from public key');
}
Copy
Ask AI
/**
* 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 = HDWallet.toExtendedPublicKey(parent);
// 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)
Copy
Ask AI
// 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:Copy
Ask AI
// From parent xpub
const xpub = HDWallet.toExtendedPublicKey(root);
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 = HDWallet.getPublicKey(child0);
const pubKey1 = HDWallet.getPublicKey(child1);
console.log(pubKey0); // Uint8Array(33)
console.log(HDWallet.getPrivateKey(child0)); // null
Use Cases
1. Watch-Only Wallets:Copy
Ask AI
// 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);
}
Copy
Ask AI
// 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);
}
Copy
Ask AI
// 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
Copy
Ask AI
// 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 = HDWallet.getPrivateKey(level5);
const dir = HDWallet.getPrivateKey(direct);
console.log(seq!.every((b, i) => b === dir![i])); // true
Multi-Account Derivation
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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 = HDWallet.getPrivateKey(child1);
const priv2 = HDWallet.getPrivateKey(child2);
console.log(priv1!.every((b, i) => b === priv2![i])); // true
Reproducible Wallets
Copy
Ask AI
// 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
Copy
Ask AI
/**
* 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 = HDWallet.getChainCode(root);
console.log(chainCode); // Uint8Array(32)
// Child derivation uses parent's chain code
// I = HMAC-SHA512(parent_chain_code, data)
Chain Code Secrecy
Copy
Ask AI
/**
* 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 = HDWallet.toExtendedPublicKey(root);
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
const xpub = HDWallet.toExtendedPublicKey(root);
const pubOnly = HDWallet.fromPublicExtendedKey(xpub);
try {
HDWallet.deriveChild(pubOnly, HARDENED + 0);
} catch (error) {
console.error('Cannot derive hardened from public key');
}
Invalid Path Format
Copy
Ask AI
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 LevelsCopy
Ask AI
// ✅ 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";
Copy
Ask AI
// ✅ 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}`);
}
Copy
Ask AI
function safeDeriveChild(parent: ExtendedKey, index: number): ExtendedKey {
if (!HDWallet.canDeriveHardened(parent) && index >= HARDENED) {
throw new Error('Cannot derive hardened from public key');
}
return HDWallet.deriveChild(parent, index);
}

