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:
- Child Keys - Sequential child key derivation
- Hardened Derivation - Hardened vs non-hardened comparison
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');
}
/**
* 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);
}
// 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);
}
// 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";
// ✅ 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}`);
}
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);
}

