OYLZ
博客游戏OY_UIPixelForge
备案信息: 粤ICP备18091077号-1

Web3 钱包的创建:从助记词到以太坊地址

2026/1/13 14:49:0
Web3

创建一个钱包时,我们通常会得到一组助记词。之后,即使更换设备,只要输入这组助记词,也能恢复原来的地址。这背后常用的一套方案,就是 BIP39、BIP32 和 BIP44。

它们分别解决不同的问题:

规范作用
BIP39生成助记词,并由助记词和可选口令派生 seed
BIP32从 seed 派生出树状结构的密钥
BIP44约定密钥树中各层的用途

三者组合起来,可以实现一种常见的分层确定性钱包,也就是 HD Wallet。不过,HD 钱包并不必须使用 BIP39 或 BIP44,它们分别提供了助记词方案和路径约定。BIP39、BIP32、BIP44

本文以通过私钥控制的以太坊外部账户(EOA)为例,介绍从助记词生成地址、恢复地址,以及查询余额的过程。

从一个 seed 派生多个密钥

BIP32 定义了 Hierarchical Deterministic Wallet,即分层确定性钱包。

“分层”指密钥之间存在父子关系,组成一棵树;“确定性”指在 seed 和派生路径相同的情况下,总能得到相同的密钥。

因此,钱包不需要单独备份每一个派生出来的私钥,可以从同一个 seed 重新计算它们。实际恢复时,还需要知道或由钱包识别对应的派生路径与地址规则。

BIP32 中的节点不仅包含密钥,还包含一个参与后续派生的 chain code。密钥和 chain code 组合起来,构成扩展私钥或扩展公钥。

扩展私钥可以派生后代私钥;扩展公钥可以派生非硬化路径上的后代公钥。这让系统可以在不提供私钥的情况下,生成收款地址,实现只读观察和签名能力的分离。这里的能力分离来自密钥派生规则,并不是链上的角色权限系统。BIP32

用派生路径定位密钥

BIP44 基于 BIP32 和 BIP43,为密钥树的各层赋予了明确含义:

m / purpose' / coin_type' / account' / change / address_index

其中,m 表示主私有节点,其余部分含义如下:

层级含义
purpose'在 BIP44 中固定为 44',标识这棵子树使用 BIP44 规则
coin_type'币种类型,用于划分不同币种的密钥空间
account'账户索引,从 0 开始
change0 表示对外收款分支,1 表示内部找零分支
address_index分支中的地址索引,从 0 开始

这里的“外部链”和“内部链”是密钥树中的分支。内部找零分支主要对应比特币等使用 UTXO 模型的场景。BIP44

需要区分的是,BIP44 的 purpose 始终是 44'。例如,比特币原生隔离见证地址可以使用 84',但那属于 BIP84,并不是 BIP44 对比特币的特殊取值。BIP84

币种编号可以在 SLIP-0044 中查询。例如,比特币是 0,以太坊是 60,写在硬化派生路径中时分别表示为 0' 和 60'。这个编号不是连接网络时使用的 Chain ID。SLIP-0044

以太坊中常见的第一个地址路径是:

m/44'/60'/0'/0/0

一些钱包也会在多个 EVM 网络中复用这条路径。例如,Phantom 的一组派生规则会为 Ethereum、Polygon 和 Base 使用同样的以太坊路径。这属于钱包的实现约定,不能据此假设所有 EVM 钱包的路径都相同。Phantom 派生路径说明

路径中的单引号 ' 表示硬化派生。硬化子节点的派生需要父私钥,不能仅从父扩展公钥完成。在 BIP44 中,前三层使用硬化派生,后两层使用普通派生。BIP32

还有一个容易混淆的细节:钱包界面中的“账户”,不一定对应路径中的 account'。

例如,钱包界面里的三个账户,可能分别使用:

m/44'/60'/0'/0/0
m/44'/60'/0'/0/1
m/44'/60'/0'/0/2

这里增加的是最后一层 address_index,而 account' 始终为 0'。ethers 的文档也专门区分了增加账户层与增加地址索引这两种路径习惯。ethers HD 钱包文档

BIP44 确实建议:当前一个账户没有交易历史时,软件应阻止创建后续账户。这个约定是为了让恢复程序能够顺序发现账户,并在遇到未使用的账户时停止。判断依据是交易历史,而不是当前余额。它并不是派生算法的限制,也不能直接套用到所有钱包界面的“添加账户”操作。BIP44 账户发现规则

从随机数生成助记词

BIP39 定义了两个过程:先生成助记词,再从助记词派生 seed。

以 12 个英文单词的助记词为例:

  1. 使用密码学安全的随机数生成器,生成 128 位随机熵。
  2. 对这些熵字节计算 SHA-256,取结果的前 4 位作为校验位。
  3. 将校验位追加到熵后面,得到 132 位数据。
  4. 每 11 位分成一组,共得到 12 组。
  5. 每组表示一个 0~2047 的索引,从包含 2048 个词的词表中查出对应单词。

这些单词允许重复,但顺序不能打乱。

BIP39 也支持 15、18、21 和 24 个词,分别对应 160、192、224 和 256 位初始熵。除了英文,还有中文、日语等词表;考虑钱包之间的兼容性,规范建议优先使用英文词表。BIP39

接下来,助记词与可选的 passphrase 一起,通过 PBKDF2-HMAC-SHA512 派生出一个 64 字节的 seed。这个过程使用 2048 次迭代,并按规范对输入进行 NFKD 规范化;未设置 passphrase 时,使用空字符串。

因此,完整流程是:

随机熵 + 校验位
       ↓
     助记词 + 可选的 passphrase
       ↓
      seed
       ↓
  BIP32 主节点
       ↓
  按指定路径派生私钥
       ↓
   公钥 → 地址

passphrase 不是钱包应用的解锁密码。它会直接影响 seed:同一组助记词配上不同 passphrase,会得到不同的钱包;输错 passphrase 也可能得到一个正常但完全不同的地址。BIP39:从助记词到 seed

用 JavaScript 实现

下面使用 Node.js,保留助记词生成和密钥派生的分步过程:

  • bip39:生成、校验助记词,并派生 seed。
  • hdkey:实现 BIP32 密钥派生。
  • ethers:从私钥构造以太坊钱包对象,并通过 RPC 查询链上状态。

安装依赖:

npm install bip39 hdkey ethers@6

创建 wallet-demo.mjs 文件:

import bip39 from 'bip39'
import HDKey from 'hdkey'
import { Wallet, hexlify } from 'ethers'

const {
  setDefaultWordlist,
  generateMnemonic,
  validateMnemonic,
  mnemonicToSeedSync,
} = bip39

setDefaultWordlist('english')

// 生成 128 位随机熵对应的 12 词助记词。
const mnemonic = generateMnemonic(128)

// BIP39 可选口令;本例不设置。
const passphrase = ''

// 本例仅处理英文 BIP39 助记词。
function createMasterNode(phrase, password = '') {
  if (!validateMnemonic(phrase)) {
    throw new Error('助记词未通过 BIP39 校验')
  }

  // 返回 Buffer,不需要先转为十六进制再转回来。
  const seed = mnemonicToSeedSync(phrase, password)

  return HDKey.fromMasterSeed(seed)
}

function deriveEthereumWallet(masterNode, path) {
  const childNode = masterNode.derive(path)

  if (!childNode.privateKey) {
    throw new Error('派生节点不包含私钥')
  }

  return new Wallet(hexlify(childNode.privateKey))
}

const masterNode = createMasterNode(mnemonic, passphrase)
const path = "m/44'/60'/0'/0/0"

const wallet = deriveEthereumWallet(masterNode, path)

console.log('派生路径:', path)
console.log('以太坊地址:', wallet.address)

// 使用相同助记词、passphrase 和路径恢复地址。
const restoredMasterNode = createMasterNode(mnemonic, passphrase)
const restoredWallet = deriveEthereumWallet(restoredMasterNode, path)

console.log('恢复地址一致:', restoredWallet.address === wallet.address)

// 演示增加 address_index,派生另一个地址。
const secondPath = "m/44'/60'/0'/0/1"
const secondWallet = deriveEthereumWallet(masterNode, secondPath)

console.log('第二个地址:', secondWallet.address)

运行:

node wallet-demo.mjs

这里使用了 hdkey 完成 BIP32 派生,因此不需要再单独安装 bip32。相关方法可以在 bip39 文档、hdkey 文档 和 ethers 钱包文档 中查阅。

validateMnemonic() 检查的是词表、长度和校验位等格式要求。通过校验不代表它属于某个用户,也不代表它一定是由安全随机数生成的。

这个脚本仅演示内存中的生成和恢复,没有保存助记词。每次重新运行都会生成新的助记词和地址。真正的钱包还需要提供安全的备份与存储流程。

地址是怎样产生的

获得派生私钥后,以太坊使用 secp256k1 曲线生成公钥,再对未压缩公钥的 64 字节坐标部分计算 Keccak-256,取结果的最后 20 字节作为地址。显示时通常加上 0x 前缀。

库已经封装了这个过程,因此示例中直接读取 wallet.address 即可。

到这里,整个过程都可以离线完成。生成密钥和地址不需要先向区块链注册,也不需要支付手续费。资产余额记录在链上,钱包保存和使用的是控制账户所需的密钥。以太坊账户说明

通过节点查询余额

钱包对象能够从私钥计算出地址,但当前余额属于链上状态,需要连接节点查询。

原代码中的:

ethAccount.eth.getBalance(address)

不能使用,因为 @ethereumjs/wallet 提供的是密钥和 keystore 等工具,没有这样的 eth 查询接口。EthereumJS Wallet 文档

使用 ethers v6 时,可以通过 JsonRpcProvider 查询余额。将下面代码追加到前面的文件末尾:

import { JsonRpcProvider, formatEther } from 'ethers'

// 在运行环境中设置 ETH_RPC_URL,指向以太坊主网 RPC。
const rpcUrl = process.env.ETH_RPC_URL

if (!rpcUrl) {
  console.log('未设置 ETH_RPC_URL,跳过余额查询')
} else {
  const provider = new JsonRpcProvider(rpcUrl)

  try {
    const network = await provider.getNetwork()

    // 本例查询以太坊主网 ETH,先确认连接的网络。
    if (network.chainId !== 1n) {
      throw new Error('请使用以太坊主网 RPC')
    }

    const balanceWei = await provider.getBalance(wallet.address)

    console.log('余额(wei):', balanceWei.toString())
    console.log('余额(ETH):', formatEther(balanceWei))
  } catch (error) {
    console.error(
      '余额查询失败:',
      error instanceof Error ? error.message : '未知错误',
    )
  } finally {
    provider.destroy()
  }
}

getBalance() 返回以 wei 为单位的 bigint,formatEther() 将其转换为适合展示的 ETH 字符串。查询只需要地址,无须向 RPC 节点提供助记词、seed 或私钥。ethers Provider 文档、单位转换文档

同一个地址在不同网络上的余额需要分别查询。切换 RPC 网络并不会把资产转移到另一条链上。

恢复钱包时,需要保留哪些信息

从上面的过程可以看出,恢复同一个派生地址,需要保持这些输入和规则一致:

  • 原来的助记词及单词顺序。
  • 原来的 BIP39 passphrase;没有设置时使用空字符串。
  • 原来的派生路径。
  • 对应的密钥与地址生成规则。

这也解释了为什么有时助记词输入正确,新钱包却没有显示原来的地址:两个钱包可能采用了不同路径,或没有扫描到对应索引。MetaMask 的导入说明也明确提示了派生路径的兼容范围。MetaMask 导入说明

备份单个派生私钥,只能恢复它对应的账户,不能据此恢复整个 HD 钱包。完整备份应围绕助记词及其 passphrase 展开,并保留必要的路径信息。

助记词、seed 和私钥都不应进入日志、代码仓库或分析上报系统。示例也没有打印这些内容。实际产品除了派生算法,还需要认真处理随机数来源、密钥加密存储、备份确认和签名流程。

当这些环节连接起来,我们才从“生成一个地址”的演示,走向一个能够安全创建、恢复并使用账户的钱包。