教程区块链区块链技术ch022.6 钱包与密钥管理:从熵到地址

本页目录

2.5 节完成了数字签名的数学实现。本节转入工程实践——如何将密码学安全私钥安全地生成、存储和使用,这是用户与区块链交互的第一入口。钱包不是"存放币的地方",而是密钥的管理器

2.6.1 密钥生成的起点:密码学安全随机数

为什么需要 CSPRNG?

普通伪随机数生成器(如 Math.random())是可预测的。如果你用 Math.random() 生成私钥,攻击者可以通过观察少量输出推断出整个序列,从而推算出你的私钥。

密码学安全随机数生成器(CSPRNG) 的要求:

  1. 前向不可预测性:即使攻击者知道前 nn 个输出,也无法预测第 n+1n+1 个。
  2. 后向不可预测性:即使攻击者知道后续输出,也无法推算出之前的内部状态(防止逆向推导种子)。
typescript
/**
 * 密码学安全随机数生成(基于浏览器/Node crypto API)
 */
function secureRandomBytes(size: number): Uint8Array {
  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
    // 浏览器环境
    return crypto.getRandomValues(new Uint8Array(size));
  } else if (typeof require === 'function') {
    // Node.js 环境
    const nodeCrypto = require('crypto');
    return nodeCrypto.randomBytes(size);
  }
  throw new Error('No CSPRNG available');
}

// 生成一个 secp256k1 私钥:256 位随机数,需满足 1 <= d < n
function generatePrivateKey(): bigint {
  const n = SECP256K1.n;
  while (true) {
    const bytes = secureRandomBytes(32);
    let d = 0n;
    for (let i = 0; i < 32; i++) {
      d = (d << 8n) | BigInt(bytes[i]);
    }
    if (d >= 1n && d < n) return d;
  }
}

console.log("=== 安全私钥生成 ===");
const d = generatePrivateKey();
console.log(`私钥: 0x${d.toString(16).padStart(64, '0')}`);
console.log(`位数: ${d.toString(2).length}`);
console.log(`有效范围: ${d > 0n && d < SECP256K1.n ? '✅ 有效' : '❌ 无效'}`);

熵的物理来源

crypto.randomBytes 的核心是操作系统内核收集的真实熵源

  • 硬件中断时间(键盘按键、磁盘 I/O、网络包到达时间)
  • CPU 硬件随机数生成器(如 Intel RDRAND 指令)
  • 其他不可预测的硬件事件

关键规则:永远不要自己写随机数生成器。"密码学家唯一会自己实现的随机数生成器,就是另一个会被攻破的随机数生成器。"

2.6.2 BIP-39:从随机熵到人类可记忆的助记词

flowchart LR
    RNG[密码学安全随机数<br/>熵 128-256 bit] --> MSE[助记词<br/>12-24 个英文单词]
    MSE --> Seed[种子<br/>PBKDF2-HMAC-SHA512<br/>2000 轮迭代]
    Seed --> XK[主扩展密钥<br/>BIP-32]
    XK --> Chain[子密钥派生<br/>CKD: 父密钥 + 索引]
    Chain --> Tree[HD 派生树<br/>BIP-44: m/44'/0'/0'/i]
    Tree --> Addr[比特币/以太坊地址]
    style MSE fill:#fff3e0
    style XK fill:#e3f2fd
    style Addr fill:#c8e6c9

为什么需要助记词?

256 位的十六进制字符串(64 个字符)对人类极不友好。抄错一个字符 = 资产永久丢失。BIP-39 将 128–256 位的熵映射为 12–24 个英文单词(从一个 2048 词的固定词表中选取),大幅降低抄写错误率。

BIP-39 的完整流程

text
128-256 位随机熵
    │
    ▼
┌─────────────────┐
│  SHA-256 哈希    │ → 取前 (熵位数/32) 位作为"校验和"
└─────────────────┘
    │
    ▼
熵 + 校验和 = 264-330 位
    │
    ▼ 每 11 位 = 一个单词索引(0-2047)
12-24 个助记词
typescript
/**
 * BIP-39 助记词简化实现
 * 使用 128 位熵 = 12 词 + 4 位校验和
 */
const BIP39_WORD_LIST: string[] = [
  "abandon", "ability", "able", // ... 完整词表有 2048 个词
  // 教学演示:使用前 64 个词做简化版
  ...Array.from({ length: 2048 }, (_, i) => `word${i.toString(16).padStart(3, '0')}`) // 占位符
];

// 真实词表应使用标准英文词表(如 bitcoin/bips 仓库中的 bip-0039/english.txt)

class BIP39 {
  private wordList: string[];
  
  constructor(wordList: string[] = BIP39_WORD_LIST) {
    if (wordList.length !== 2048) throw new Error('Word list must have 2048 entries');
    this.wordList = wordList;
  }
  
  /**
   * 熵 → 助记词
   * @param entropy 128, 160, 192, 224, 或 256 位的 Uint8Array
   */
  entropyToMnemonic(entropy: Uint8Array): string {
    const ENT = entropy.length * 8; // 熵位数
    const CS = ENT / 32; // 校验和位数
    const totalBits = ENT + CS; // 总位数 = 11 的倍数
    
    if (![128, 160, 192, 224, 256].includes(ENT)) {
      throw new Error('Entropy must be 128/160/192/224/256 bits');
    }
    
    // 计算 SHA-256 并取前 CS 位作为校验和
    // 教学简化:假设已有 sha256 函数
    const hash = sha256(new TextDecoder().decode(entropy));
    const hashBits = BigInt('0x' + hash).toString(2).padStart(256, '0');
    const checksumBits = hashBits.slice(0, CS);
    
    // 将完整数据转为二进制字符串
    let dataBits = '';
    for (const byte of entropy) {
      dataBits += byte.toString(2).padStart(8, '0');
    }
    dataBits += checksumBits;
    
    // 每 11 位 = 一个词
    const words: string[] = [];
    for (let i = 0; i < dataBits.length; i += 11) {
      const index = parseInt(dataBits.slice(i, i + 11), 2);
      words.push(this.wordList[index]);
    }
    
    return words.join(' ');
  }
  
  /**
   * 助记词 → 种子(通过 PBKDF2 密钥派生)
   * 真实现象中应使用该函数结合盐值 "mnemonic" 迭代 2048 次
   */
  mnemonicToSeed(mnemonic: string, passphrase: string = ''): Uint8Array {
    // 教学简化:实际应使用 PBKDF2-HMAC-SHA512
    // 标准: seed = PBKDF2(mnemonic, "mnemonic" + passphrase, 2048, 512)
    const normalized = mnemonic.normalize('NFKD');
    const salt = ('mnemonic' + passphrase).normalize('NFKD');
    // 返回 512 位种子
    return secureRandomBytes(64); // 占位:真实需 PBKDF2 实现
  }
}

// --- BIP-39 验证 ---
const bip39 = new BIP39();
const entropy = secureRandomBytes(16); // 128 位 = 12 词
console.log(`\n128 位熵: ${Array.from(entropy).map(b => b.toString(16).padStart(2, '0')).join('')}`);
// const mnemonic = bip39.entropyToMnemonic(entropy);
// console.log(`助记词: ${mnemonic}`);

助记词的安全强度

助记词长度熵位数暴力尝试次数安全性
12 词128 位21282^{128}极安全(当前算力不可行)
15 词160 位21602^{160}极高
18 词192 位21922^{192}后量子安全级别
21 词224 位22242^{224}过度安全
24 词256 位22562^{256}过度安全

标准推荐:12 词(128 位)对大多数用户足够安全且便于记忆/抄写。24 词主要用于要求极致安全的场景(如机构冷存储)。

2.6.3 BIP-32/BIP-44:层级确定性钱包(HD Wallet)

为什么需要 HD 钱包?

传统钱包为每笔交易生成独立随机私钥。用户需要备份每一个私钥——使用 100 次 = 备份 100 个私钥。HD 钱包(BIP-32, 2012)解决了这个灾难:从一个主种子通过确定性算法派生无限多个子密钥,只需备份一次种子(助记词)。

核心思想:扩展密钥 + 子密钥派生

text
主种子 (64 字节, 512 位)
    │
    ▼
┌──────────────┐
│ HMAC-SHA512  │
│ key="Bitcoin seed" │
└──────────────┘
    │
    ├─ 主私钥 (256 位) → 主公钥
    └─ 主链码 (256 位) → 用于子密钥派生

子密钥派生函数(CKD, Child Key Derivation)

\text{child}_i = HMAC\text{-}SHA512(\text{parent_chain_code}, \text{parent_key} \parallel i)

输出分为两半:左半部分作为子私钥,右半部分作为子链码。通过递增索引 ii,可以生成无限多个独立的子密钥。

typescript
/**
 * 简化的 BIP-32 层级派生(教学演示)
 */
class HDNode {
  privateKey: bigint | null; // 仅"扩展私钥"节点持有
  publicKey: ECPoint;
  chainCode: Uint8Array;
  depth: number;
  index: number;
  parentFingerprint: number;

  constructor(
    privateKey: bigint | null,
    publicKey: ECPoint,
    chainCode: Uint8Array,
    depth: number = 0,
    index: number = 0,
    parentFingerprint: number = 0,
  ) {
    this.privateKey = privateKey;
    this.publicKey = publicKey;
    this.chainCode = chainCode;
    this.depth = depth;
    this.index = index;
    this.parentFingerprint = parentFingerprint;
  }

  /**
   * 从种子创建 HD 钱包根节点
   */
  static fromSeed(seed: Uint8Array): HDNode {
    // 真实实现: I = HMAC-SHA512(key="Bitcoin seed", data=seed)
    // 简化演示
    const I = secureRandomBytes(64); // 占位
    const IL = I.slice(0, 32); // 主私钥
    const IR = I.slice(32, 64); // 主链码
    
    let masterKey = 0n;
    for (let i = 0; i < 32; i++) {
      masterKey = (masterKey << 8n) | BigInt(IL[i]);
    }
    
    const pubKey = scalarMultiply(masterKey, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
    return new HDNode(masterKey, pubKey, IR, 0, 0, 0);
  }

  /**
   * 派生子节点(简化版,省略硬化派生的细节)
   */
  derive(index: number): HDNode {
    if (this.depth >= 255) throw new Error('Max depth exceeded');
    
    // 真实实现需根据 hardened (index >= 2^31) 或 normal 模式选择不同的派生方式
    const childKey = (this.privateKey! + BigInt(index)) % SECP256K1.n; // 教学简化
    const childPubKey = scalarMultiply(childKey, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
    
    return new HDNode(
      childKey,
      childPubKey,
      this.chainCode, // 简化:实际需重新计算
      this.depth + 1,
      index,
      0, // 简化:实际需计算父节点指纹
    );
  }
}

// --- 演示:从种子到无限地址 ---
const seed = secureRandomBytes(64);
const root = HDNode.fromSeed(seed);
console.log(`\n=== BIP-32 链式推导 ===`);
console.log(`根公钥: ${root.publicKey.toString()}`);

const child0 = root.derive(0);
console.log(`派生子 0: ${child0.publicKey.toString()}`);

const child1 = root.derive(1);
console.log(`派生子 1: ${child1.publicKey.toString()}`);

// 同样的种子总是产生同样的子密钥(确定性)

BIP-44:标准化的派生路径

BIP-44 定义了五层派生路径标准:

m / \text{purpose}' / \text{coin_type}' / \text{account}' / \text{change} / \text{address_index}
层级含义示例
m主密钥
44'目的:BIP-44 标准固定
0'币种类型:0=比特币,60=以太坊,501=Solana
0'账户编号:0 开始多账户管理
0外部链(0=收款,1=找零)比特币找零机制
0地址索引每用一次 +1

示例路径

  • 比特币首个外部地址:m/44'/0'/0'/0/0
  • 以太坊首个地址:m/44'/60'/0'/0/0
  • 比特币第 10 个外部地址:m/44'/0'/0'/0/9
typescript
/**
 * BIP-44 路径解析器
 */
function parseBIP44Path(path: string): number[] {
  const parts = path.split('/');
  if (parts[0] !== 'm') throw new Error('Path must start with m');
  
  const indices: number[] = [];
  for (let i = 1; i < parts.length; i++) {
    const part = parts[i];
    const isHardened = part.endsWith("'");
    const index = parseInt(isHardened ? part.slice(0, -1) : part, 10);
    const finalIndex = isHardened ? index + 0x80000000 : index;
    indices.push(finalIndex);
  }
  return indices;
}

console.log(`\nBIP-44 路径 m/44'/0'/0'/0/0 解析: [${parseBIP44Path("m/44'/0'/0'/0/0").join(', ')}]`);
// 硬化索引通过在第 31 位加 1 区分:0x80000000 = 2^31

2.6.4 从公钥到地址:比特币与以太坊的差异

比特币地址(P2PKH)

\text{地址} = \text{Base58Check}(\text{RIPEMD160}(\text{SHA256}(\text{pubkey})) \oplus \text{network_byte})
  • 04 前缀(未压缩公钥)→ SHA256RIPEMD160(20 字节哈希)→ 加版本字节 0x00(主网)→ Base58Check 编码。
  • 以 "1" 开头的是主网 P2PKH 地址(如 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)。

以太坊地址

地址=Keccak256(pubkey)[12:32]\text{地址} = \text{Keccak256}(\text{pubkey})[12:32]
  • 取公钥(64 字节未压缩,去掉 0x04)的 Keccak-256 哈希,取最后 20 字节。
  • 0x 开头,40 个十六进制字符(如 0xdAC17F958D2ee523a2206206994597C13D831ec7)。
  • 无 Base58Check——原始十六进制,但包含 EIP-55 大小写校验。
typescript
/**
 * 公钥到地址的两种路线
 */
function bitcoinAddressFromPubkey(pubKey: ECPoint): string {
  // 简化:仅展示流程
  const pubKeyBytes = new Uint8Array(65); // 0x04 + x + y
  // ... 序列化公钥位为字节
  // hash160 = RIPEMD160(SHA256(pubKeyBytes))
  // address = Base58Check(0x00 || hash160)
  return "1A1z...P1eP"; // 占位
}

function ethereumAddressFromPubkey(pubKey: ECPoint): string {
  // 未压缩公钥去掉 0x04 后的 64 字节
  const pubKeyBytes = new Uint8Array(64);
  // ... 序列化 x, y 为字节
  // address = Keccak256(pubKeyBytes).slice(-20)
  return "0x..."; // 占位
}
特性比特币 P2PKH以太坊
哈希函数SHA256 → RIPEMD160Keccak-256
输出大小160 位160 位(Keccak-256 的最后 160 位)
编码Base58Check十六进制(EIP-55 大小写校验)
前缀"1"(主网)"0x"
大小写敏感不敏感EIP-55 校验依赖大小写

2.6.5 冷存储与硬件钱包的安全实践

威胁模型

威胁热钱包(联网软件)冷钱包(离线)
操作系统漏洞/恶意软件❌ 高风险✅ 免疫
网络钓鱼❌ 可能误签✅ 需物理确认
物理盗窃❓ 依赖设备密码❓ 依赖物理安全
用户误操作高(需确认多个步骤)

硬件钱包的核心机制

硬件钱包(如 Ledger、Trezor)的核心安全保证是:

私钥从不出现在硬件设备的"易泄露区域"(RAM/CPU 缓存可能被侧信道攻击),且永远不暴露给连接的主机。

交易签名流程:

  1. 主机将待签名的交易数据发送给硬件设备。
  2. 硬件在屏幕上显示交易内容(如"发送 0.5 BTC 到 1A1z...")。
  3. 用户物理按下设备上的确认按钮。
  4. 设备使用芯片内安全元素的私钥执行签名。
  5. 只将签名结果 (r,s)(r, s) 返回给主机,私钥从未离开设备。

助记词的安全备份

永远不要

  • 将助记词存储在联网设备(手机照片、云盘、邮件)。
  • 将助记词输入任何网站或应用(除非是首次在新设备上恢复钱包)。
  • 只保留一份备份(单点故障)。

推荐做法

  • 写在金属板(防火防水)上,存放在两个不同物理位置。
  • 或使用Shamir 秘密共享(BIP-39 扩展):将 24 词分成 3 份,任意 2 份即可恢复,避免单点失窃/丢失。

核心认知

  1. 钱包 = 密钥管理器。 它不"存放"币,币永远在区块链上。钱包只是保存了授权花费这些币的私钥。
  1. BIP-39 助记词是人类可记忆的 128 位熵。 校验和机制保证 12 个词中抄错任意一个词可以立即被检测(约 1/256 的错误会被校验和捕获,约 255/256 的错误会被词表检查捕获)。
  1. BIP-32 推导 = 一次备份,无限密钥。 主种子派生子密钥的树状结构,使得企业可以批量管理数千个客户存款地址,个人可以在多账户间隔离隐私。
  1. 冷存储的核心不是技术,而是物理隔离。 硬件钱包的价值在于"私钥永远不会暴露在联网环境中"。空气隔离的计算机 + 离线签名 + 二维码传输,是机构级冷存储的标准做法。

下一预告:2.7 节将探索默克尔树(Merkle Tree)——如何将数百万笔交易压缩为一个 32 字节的根哈希,以及轻客户端(SPV)如何在不下载完整区块链的情况下验证交易存在性。

评论

0

评论加载中…

发表评论

0/2000