钱包是很多人第一次接触 Ethereum 或其他虚拟货币的地方。不管是用手机或浏览器的钱包,相信很多人都对一串陌生的单字感到好奇(而且很重要还要备份)。这是源自于 Bitcoin 中钱包的设计,采用这套机制的钱包通常称为 HD Wallet。本篇希望简述 HD Wallet 的架构,再使用 JavaScript 套件从头创建一个 Ethereum HD Wallet。
虚拟货币钱包
钱包顾名思义是存放$$$。但在虚拟货币世界有点不一样,我的帐户资讯(像是我有多少钱)是储存在区块链上,实际存在钱包中的是我的帐户对应的 key。有了这把 key 我就可以在虚拟货币世界证明我的身份、就可以更改我帐户的状态(像是送钱给别人)。这样来说,虚拟货币钱包实际上是管理和储存 key 的工具。这把 key 就是我的私钥,而帐户是从我的公钥衍伸出来。
BIP32, BIP39, BIP44
BIP 全名是 Bitcoin Improvement Proposals,是提出 Bitcoin 的新功能或改进措施的文件。可由任何人提出,经过审核后公布在 bitcoin/bips 上。BIP 和 Bitcoin 的关系,就像是 RFC 之于 Internet。
而其中的 BIP32, BIP39, BIP44 共同定义了目前被广泛使用的 HD Wallet,包含其设计动机和理念、实作方式、实例等。
- BIP32:定义 Hierarchical Deterministic wallet (简称 "HD Wallet"),是一个系统可以从单一个 seed 产生一树状结构储存多组 keypairs(私钥和公钥)。好处是可以方便的备份、转移到其他相容装置(因为都只需要 seed),以及分层的权限控制等。
-
BIP39:将 seed 用方便记忆和书写的单字表示。一般由 12 个单字组成,称为 mnemonic code(phrase),中文称为助记词或助记码。例如:
rose rocket invest real refuse margin festival danger anger border idle brown
-
BIP44:基于 BIP32 的系统,赋予树状结构中的各层特殊的意义。让同一个 seed 可以支援多币种、多帐户等。各层定义如下:
m / purpose' / coin_type' / account' / change / address_index
其中的
purporse'
固定是44'
,代表使用 BIP44。而coin_type'
用来表示不同币种,例如 Bitcoin 就是0'
,Ethereum 是60'
。
Ethereum HD Wallet
Ethereum 的钱包目前均采用以上 Bitcoin HD Wallet 的架构,并订 coin_type'
为 60'
,可以在 ethereum/EIPs/issues 中看到相关的讨论。举例来说,在一个 Ethereum HD Wallet 中,第一个帐户(这里的帐户指 BIP44 中定义的 account'
)的第一组 keypair,其路径会是 m/44'/60'/0'/0/0
。
创建 Ethereum HD wallet
使用的 JavaScript 套件包含:
- bip39:实作 BIP39,随机产生新的 mnemonic code,并可以将其转成 binary 的 seed。
- ethereumjs-wallet:产生和管理公私钥,我使用其中的 hdkey 子套件来创建 HD Wallet。
- ethereumjs-util:集合许多 Ethereum 需要的运算功能。
安装套件
npm install bip39 ethereumjs-wallet ethereumjs-util --save
汇入套件
var bip39 = require('bip39')
var hdkey = require('ethereumjs-wallet/hdkey')
var util = require('ethereumjs-util')
产生 mnemonic code
var mnemonic = bip39.generateMnemonic()
取得的 mnemonic code 会像:
rose rocket invest real refuse margin festival danger anger border idle brown
产生 HD wallet
先将 mnemonic code 转成 binary 的 seed。
var seed = bip39.mnemonicToSeed(mnemonic)
使用 seed 产生 HD Wallet。如果要说更明确,就是产生 Master Key 并记录起来。
var hdWallet = hdkey.fromMasterSeed(seed)
产生第一个 Ethereum Address
产生 Wallet 中第一个帐户的第一组 keypair。可以从 Master Key,根据其路径 m/44'/60'/0'/0/0
推导出来。
var key1 = hdWallet.derivePath("m/44'/60'/0'/0/0")
使用 keypair 中的公钥产生 address。
var address1 = util.pubToAddress(key1._hdkey._publicKey, true)
取得的 Address:
685ce4cbdd5c19b64ca008cb85b83947e5318efa
Encoding Address
Ethereum 很贴心,为了避免大家打错 address(导致把钱送错人),Ethereum 让 Address 变得比较难打?!总之一般会用 EIP55: Mixed-case checksum address encoding 再进行编码。许多钱包也支援用户输入没经过编码的 Address,那就会跳过 checksum 机制,建议还是使用编码过的 Address。
address1 = util.toChecksumAddress(address1.toString('hex'))
最后取得的 Address 会像:
0x685ce4CbDd5c19b64CA008cB85b83947e5318EFA
可以用 Mnemonic Code Converter 验证结果
使用 Ethereum HD wallet
把 mnemonic code 记录下来好好保存,就会是一个冷钱包(指不连网路的钱包,所以安全很多)。可以使用产生出来的 address 收 Ether 或任何 REC20 Token。要送钱的话,可以汇入到任一个支援 Ethereum HD Wallet 的钱包。常用的 Ethereum HD wallet 像,在浏览器使用的 MyEtherWallet、MetaMask 和在手机使用的 imToken 等。
题外话,MetaMask 如何在浏览器储存我们的 mnemonic code?
相信大家都了解了,有 mnemonic code 就可以产生 HD Wallet 中所有的 keys。有了 keys 就可以任意送钱包中的 Ether 或 Token 给别人。所以 mnemonic code 很重要!!!那这么重要的东西保存在浏览器不会很危险吗?我便研究下我常用的 MetaMask 浏览器钱包。MetaMask 将加密后的 mnemonic code 存在浏览器的 Local Storage(一块只存在 Local 且不会过期的资料区块)。加密使用用户另外输入的密码,再汇入时会要求用户设定密码(如上图),而每一次重新开启钱包都会要求输入密码。解密算法有 Open Source,也有线上 Live Demo。
References
其他相关 Ethereum JavaScript 套件
感谢 Jiyi 大大提供密码学专业知识,虽然详细的数学计算本篇没有提到,但让我有底气的完成这篇文章。