跳至主要內容
進階

HD Wallets

階層確定性錢包:BIP-32/39/44/84/86 密鑰推導標準

20 分鐘

概述

階層確定性錢包(Hierarchical Deterministic Wallet)允許從單一種子 派生無限數量的密鑰對,同時只需備份一個助記詞。 這是現代比特幣錢包的標準實現方式。

核心優勢: 12/24 個助記詞可以恢復整個錢包的所有地址和交易歷史, 無需備份每個私鑰。

BIP-39: 助記詞

助記詞生成

助記詞生成流程:

1. 生成熵 (Entropy)
   - 128 bits: 12 詞
   - 160 bits: 15 詞
   - 192 bits: 18 詞
   - 224 bits: 21 詞
   - 256 bits: 24 詞

2. 添加校驗和
   checksum = SHA256(entropy)[0:entropy_bits/32]
   total_bits = entropy_bits + checksum_bits

3. 分割為 11 位組
   每 11 位對應詞表中的一個單詞
   2^11 = 2048 個單詞

範例 (128 bits):
熵:          128 bits
校驗和:        4 bits (128/32)
總計:        132 bits
單詞數:       12 個 (132/11)

從助記詞到種子

import { pbkdf2 } from '@noble/hashes/pbkdf2';
import { sha512 } from '@noble/hashes/sha512';

// BIP-39 助記詞到種子
function mnemonicToSeed(
  mnemonic: string,
  passphrase: string = ''
): Uint8Array {
  const mnemonicBytes = new TextEncoder().encode(
    mnemonic.normalize('NFKD')
  );
  const salt = new TextEncoder().encode(
    ('mnemonic' + passphrase).normalize('NFKD')
  );

  // PBKDF2-HMAC-SHA512, 2048 輪
  return pbkdf2(sha512, mnemonicBytes, salt, {
    c: 2048,
    dkLen: 64,
  });
}

// 驗證助記詞校驗和
function validateMnemonic(mnemonic: string): boolean {
  const words = mnemonic.trim().split(/\s+/);

  if (![12, 15, 18, 21, 24].includes(words.length)) {
    return false;
  }

  // 檢查每個詞是否在詞表中
  for (const word of words) {
    if (!WORDLIST.includes(word)) {
      return false;
    }
  }

  // 驗證校驗和
  const bits = words.map(w => WORDLIST.indexOf(w))
    .map(i => i.toString(2).padStart(11, '0'))
    .join('');

  const entropyBits = bits.slice(0, -bits.length / 33);
  const checksumBits = bits.slice(-bits.length / 33);

  const entropy = new Uint8Array(
    entropyBits.match(/.{8}/g)!.map(b => parseInt(b, 2))
  );

  const hash = sha256(entropy);
  const expectedChecksum = [...hash]
    .map(b => b.toString(2).padStart(8, '0'))
    .join('')
    .slice(0, checksumBits.length);

  return checksumBits === expectedChecksum;
}

BIP-32: 密鑰推導

擴展密鑰

Extended Key 結構 (78 bytes):

┌─────────────────────────────────────────────────────────┐
│ Version    │ Depth │ Fingerprint │ Child # │ Chaincode │
│  4 bytes   │ 1 byte│   4 bytes   │ 4 bytes │ 32 bytes  │
├─────────────────────────────────────────────────────────┤
│                        Key                              │
│                      33 bytes                           │
│         (私鑰加 0x00 前綴 或 壓縮公鑰)                   │
└─────────────────────────────────────────────────────────┘

Version bytes:
- xprv: 0x0488ADE4 (mainnet private)
- xpub: 0x0488B21E (mainnet public)
- tprv: 0x04358394 (testnet private)
- tpub: 0x043587CF (testnet public)

SegWit 版本:
- yprv/ypub: P2SH-P2WPKH (BIP-49)
- zprv/zpub: P2WPKH (BIP-84)

Taproot 版本:
- xprv/xpub: P2TR (使用不同推導路徑)

子密鑰推導

推導類型:

正常推導 (Normal/Non-hardened):
- 索引: 0 到 2^31 - 1
- 可從 xpub 推導子 xpub
- 路徑表示: m/0/1/2

硬化推導 (Hardened):
- 索引: 2^31 到 2^32 - 1
- 需要私鑰才能推導
- 路徑表示: m/0'/1'/2' 或 m/0h/1h/2h

安全性差異:
┌─────────────────────────────────────────────────────────┐
│ 如果子私鑰和 xpub 洩露:                                 │
│                                                         │
│ 正常推導: 可計算父私鑰和所有兄弟私鑰!                  │
│ 硬化推導: 其他密鑰仍然安全                              │
└─────────────────────────────────────────────────────────┘

TypeScript 實作

import { hmac } from '@noble/hashes/hmac';
import { sha512 } from '@noble/hashes/sha512';
import * as secp256k1 from '@noble/secp256k1';

interface ExtendedKey {
  version: Uint8Array;
  depth: number;
  fingerprint: Uint8Array;
  childNumber: number;
  chaincode: Uint8Array;
  key: Uint8Array;  // 33 bytes
}

// 從種子創建主密鑰
function createMasterKey(seed: Uint8Array): ExtendedKey {
  const I = hmac(sha512, 'Bitcoin seed', seed);
  const IL = I.slice(0, 32);  // 私鑰
  const IR = I.slice(32);     // 鏈碼

  // 驗證私鑰有效性
  if (!secp256k1.utils.isValidPrivateKey(IL)) {
    throw new Error('Invalid master key');
  }

  return {
    version: new Uint8Array([0x04, 0x88, 0xAD, 0xE4]),
    depth: 0,
    fingerprint: new Uint8Array(4),
    childNumber: 0,
    chaincode: IR,
    key: new Uint8Array([0x00, ...IL]),
  };
}

// 子密鑰推導
function deriveChild(
  parent: ExtendedKey,
  index: number
): ExtendedKey {
  const isHardened = index >= 0x80000000;

  let data: Uint8Array;

  if (isHardened) {
    // 硬化: 0x00 || 私鑰 || 索引
    if (parent.key[0] !== 0x00) {
      throw new Error('Cannot derive hardened child from xpub');
    }
    data = new Uint8Array([
      0x00,
      ...parent.key.slice(1),
      ...uint32BE(index),
    ]);
  } else {
    // 正常: 公鑰 || 索引
    const pubkey = parent.key[0] === 0x00
      ? secp256k1.getPublicKey(parent.key.slice(1), true)
      : parent.key;
    data = new Uint8Array([
      ...pubkey,
      ...uint32BE(index),
    ]);
  }

  const I = hmac(sha512, parent.chaincode, data);
  const IL = I.slice(0, 32);
  const IR = I.slice(32);

  // 計算子密鑰
  let childKey: Uint8Array;
  if (parent.key[0] === 0x00) {
    // 私鑰 + IL
    const parentKey = BigInt('0x' + bytesToHex(parent.key.slice(1)));
    const tweak = BigInt('0x' + bytesToHex(IL));
    const childKeyBigInt = (parentKey + tweak) % secp256k1.CURVE.n;
    childKey = new Uint8Array([
      0x00,
      ...hexToBytes(childKeyBigInt.toString(16).padStart(64, '0')),
    ]);
  } else {
    // 公鑰點加法
    const parentPoint = secp256k1.Point.fromHex(bytesToHex(parent.key));
    const tweakPoint = secp256k1.Point.fromPrivateKey(IL);
    const childPoint = parentPoint.add(tweakPoint);
    childKey = childPoint.toRawBytes(true);
  }

  // 計算父指紋
  const parentPubkey = parent.key[0] === 0x00
    ? secp256k1.getPublicKey(parent.key.slice(1), true)
    : parent.key;
  const fingerprint = hash160(parentPubkey).slice(0, 4);

  return {
    version: parent.version,
    depth: parent.depth + 1,
    fingerprint,
    childNumber: index,
    chaincode: IR,
    key: childKey,
  };
}

// 從路徑推導
function derivePath(
  master: ExtendedKey,
  path: string
): ExtendedKey {
  const segments = path.split('/');

  if (segments[0] !== 'm') {
    throw new Error('Path must start with m');
  }

  let key = master;

  for (const segment of segments.slice(1)) {
    const hardened = segment.endsWith("'") || segment.endsWith('h');
    const index = parseInt(segment.replace(/['h]$/, ''), 10);
    const childIndex = hardened ? index + 0x80000000 : index;
    key = deriveChild(key, childIndex);
  }

  return key;
}

BIP-44/49/84/86: 推導路徑

路徑標準

推導路徑格式:
m / purpose' / coin_type' / account' / change / address_index

層級說明:

層級 說明
m 主密鑰
purpose' BIP 編號 (硬化)
44' = BIP-44 (Legacy)
49' = BIP-49 (P2SH-SegWit)
84' = BIP-84 (Native SegWit)
86' = BIP-86 (Taproot)
coin_type' 幣種 (硬化)
0' = Bitcoin mainnet
1' = Bitcoin testnet
account' 帳戶索引 (硬化)
0', 1', 2'...
change 0 = 接收地址, 1 = 找零地址
address_idx 地址索引 0, 1, 2...

各 BIP 路徑範例

BIP-44 (P2PKH - Legacy):
m/44'/0'/0'/0/0  →  1ABC...
m/44'/0'/0'/0/1  →  1DEF...
m/44'/0'/0'/1/0  →  1GHI... (找零)

BIP-49 (P2SH-P2WPKH):
m/49'/0'/0'/0/0  →  3ABC...
m/49'/0'/0'/0/1  →  3DEF...

BIP-84 (P2WPKH - Native SegWit):
m/84'/0'/0'/0/0  →  bc1q...
m/84'/0'/0'/0/1  →  bc1q...

BIP-86 (P2TR - Taproot):
m/86'/0'/0'/0/0  →  bc1p...
m/86'/0'/0'/0/1  →  bc1p...

地址生成

import { bech32, bech32m } from 'bech32';
import * as crypto from 'crypto';

type AddressType = 'p2pkh' | 'p2sh-p2wpkh' | 'p2wpkh' | 'p2tr';

interface WalletAccount {
  masterSeed: Uint8Array;
  accountPath: string;
  addressType: AddressType;
}

// 生成地址
function generateAddress(
  account: WalletAccount,
  change: 0 | 1,
  index: number
): { address: string; path: string } {
  const master = createMasterKey(account.masterSeed);
  const path = `${account.accountPath}/${change}/${index}`;
  const derived = derivePath(master, path);

  const pubkey = derived.key[0] === 0x00
    ? secp256k1.getPublicKey(derived.key.slice(1), true)
    : derived.key;

  let address: string;

  switch (account.addressType) {
    case 'p2pkh':
      address = pubkeyToP2PKH(pubkey);
      break;
    case 'p2sh-p2wpkh':
      address = pubkeyToP2SHP2WPKH(pubkey);
      break;
    case 'p2wpkh':
      address = pubkeyToP2WPKH(pubkey);
      break;
    case 'p2tr':
      address = pubkeyToP2TR(pubkey);
      break;
  }

  return { address, path };
}

function hash160(data: Uint8Array): Uint8Array {
  const sha = crypto.createHash('sha256').update(data).digest();
  return new Uint8Array(
    crypto.createHash('ripemd160').update(sha).digest()
  );
}

function pubkeyToP2WPKH(pubkey: Uint8Array): string {
  const hash = hash160(pubkey);
  const words = bech32.toWords(hash);
  words.unshift(0);  // witness version 0
  return bech32.encode('bc', words);
}

function pubkeyToP2TR(pubkey: Uint8Array): string {
  // 使用 x-only 公鑰 (去掉前綴)
  const xonly = pubkey.slice(1);

  // Taproot tweak (無腳本樹)
  const tweakedPubkey = taprootTweak(xonly);

  const words = bech32m.toWords(tweakedPubkey);
  words.unshift(1);  // witness version 1
  return bech32m.encode('bc', words);
}

// 批量生成地址
function generateAddresses(
  account: WalletAccount,
  count: number,
  change: 0 | 1 = 0
): Array<{ address: string; path: string; index: number }> {
  const addresses = [];

  for (let i = 0; i < count; i++) {
    const { address, path } = generateAddress(account, change, i);
    addresses.push({ address, path, index: i });
  }

  return addresses;
}

Gap Limit

Gap Limit 概念:

問題:
- HD 錢包可以生成無限地址
- 恢復時如何知道生成多少個?

解決方案: Gap Limit
- 預設: 連續 20 個未使用地址
- 恢復時掃描直到連續 20 個空地址

掃描流程:
1. 從索引 0 開始生成地址
2. 查詢每個地址的交易歷史
3. 如果有交易,記錄最高使用索引
4. 繼續掃描直到連續 20 個未使用

錢包恢復:
m/84'/0'/0'/0/0  ✓ 有交易
m/84'/0'/0'/0/1  ✓ 有交易
m/84'/0'/0'/0/2  ✗ 無交易
...
m/84'/0'/0'/0/21 ✗ 無交易 (連續 20 個)
→ 停止掃描,恢復完成

xpub 共享

xpub 用途:

Watch-only 錢包:
- 只需 xpub 即可追蹤餘額
- 無法簽名交易
- 用於會計、監控

商家收款:
- 電商服務器只存 xpub
- 為每筆訂單生成新地址
- 私鑰離線保管

安全考量:
┌─────────────────────────────────────────────────────────┐
│ xpub 洩露影響:                                          │
│ - 所有地址隱私暴露                                      │
│ - 可追蹤所有交易                                        │
│ - 餘額完全暴露                                          │
│                                                         │
│ xpub + 任一子私鑰洩露 (非硬化推導):                     │
│ - 可計算所有私鑰!                                      │
│ - 資金完全被盜                                          │
└─────────────────────────────────────────────────────────┘

最佳實踐:
- 使用帳戶級別 xpub (m/84'/0'/0')
- 不要共享根 xpub (m)
- 考慮使用 Output Descriptors

Passphrase (25th Word)

BIP-39 Passphrase:

功能:
- 助記詞 + passphrase = 完全不同的種子
- 類似「第 25 個詞」
- 提供額外安全層

用途:
1. 合理否認 (Plausible Deniability)
   - 空 passphrase → 小額錢包(誘餌)
   - 真正 passphrase → 主要資金

2. 多帳戶
   - 同一助記詞 + 不同 passphrase
   - 完全獨立的錢包

3. 雙因素備份
   - 助記詞 + passphrase 分開保管

注意:
- passphrase 錯誤會導向有效但空的錢包
- 無法驗證 passphrase 正確性
- 必須精確記住(區分大小寫、空格等)

最佳實踐

  • 使用 BIP-84/86:優先使用 SegWit/Taproot 路徑
  • 備份助記詞:離線、多份、安全保管
  • 驗證備份:定期測試恢復流程
  • 使用 passphrase:考慮使用 passphrase 增加安全性
  • 帳戶分離:不同用途使用不同帳戶索引
  • 考慮 Descriptors:現代錢包優先使用 Output Descriptors

相關資源

已複製連結
已複製到剪貼簿