比特币学习之 HD 钱包——从熵到助记词(bip39)
概念 助记种子:将随机数编码到单词中,并使用它们创建一个种子 助记词:将随机产生的大量数
概念
-
助记种子:将随机数编码到单词中,并使用它们创建一个种子
-
助记词:将随机产生的大量数字表示成一系列单词的方式,是我们更容易的存储。
这些单词然后被用来创建一个种子,这个种子在 HD 钱包中生成扩展密钥。
换句话说:我们的钱包将使用一个大的随机数来生成我们所有的密钥和地址,同时助记词给我们一个存储这个大随机数的简便方法。
那么,如何生成助记种子呢? 分三步:
-
生成熵
-
熵生成助记词
-
助记词生成助记种子
生成熵

熵:我们可以认为熵是一个非常大的随机数,以前从来没有人产生过,将来也不会产生。同时计算机是以二进制存储数字的,所以我们可以将这个数字看成一串二进制数(eg: 10011010010001... )
bit(0 或 1)是计算机存储的最小单位
同时熵还需要满足特定的要求:
-
必须是32位的倍数: 这使得我们可以将熵分成偶数个块,然后转化成单词。
-
熵必须在128位到256位之间:因为这足以使得两个人不可能产生相同的熵。
const randomBytes = require("randombytes");
var entropy = randomBytes(16)
// 输出熵 entropy 的值
console.log(entropy.toString('hex'))
// 输出结果:
// 'fa82c8bb2fd02f82ff0114069f2e5a19'
// 它是16进制,换算成二进制,熵长度为32*4=128
注意:我们生成随机数时,不要使用函数中默认的"random"函数。它产生的随机数不够随机,是有可能破解的。我们要使用一个安全的随机数发生器。
熵生成助记词
现在,我们将熵编码成单词(即助记词)。
首先,我们要将校验位添加到熵中,校验位可以帮我们检测熵是否在传输过程中被修改。
通过对初始熵 entropy 取SHA256散列来获得校验和(CS_length= 熵长度/32,取得到的SHA256散列的前 CS_length 位),然后将校验和附加到初始熵的末尾。

接下来,我们把最终的熵分成每11位为一组,并把它们转换成十进制数,每个组编码对应一个0-2047的数字,该数字作为一个索引到wordlist,对应获得wordlist上相应索引的值。

最后,我们将这些数字转换成单词,最终合在一起作为助记句。
助记词通常在12到24个单词之间
const createHash = require("create-hash");
const bip39 = require('bip39');
// 之前我们通过随机数生成的熵
entropy = 'fa82c8bb2fd02f82ff0114069f2e5a19'
// 将熵导入到 Buffer 中
entropy = Buffer.from(entropy, 'hex');
// 将熵 entropy 的十六进制字节转化成二进制
const entropyBits = bytesToBinary([...entropy]);
//生成 检验位
const checksumBits = deriveChecksumBits(entropy);
console.log(entropyBits)
console.log(checksumBits)
// 将熵和检验位组合起来
bits = entropyBits + checksumBits
// 将最终的熵以11位为一组分割开来
const chunks = bits.match(/(.{1,11})/g);
console.log(chunks)
// 从 bit39 中获取一个 wordlist 单词列表
wordlists = bip39.wordlists['EN']
// 熵转化的数字转换成单词,将单词连接起来生成助记词
const words = chunks.map(binary => {
const index = binaryToByte(binary);
return wordlists[index];
});
mnemonic = words.join(" ")
console.log(mnemonic)
// 生成检验位的函数
function deriveChecksumBits(entropyBuffer) {
// entropyBuffer是字节,所以它的二进制长度要乘以八
const ENT = entropyBuffer.length * 8;
// CS 的长度
const CS = ENT / 32;
// 将熵进行 SHA-256 哈希
const hash = createHash('sha256')
.update(entropyBuffer)
.digest();
// 将hash的字节转化成二进制,然后截取前CS位的二进制进行返回
return bytesToBinary([...hash]).slice(0, CS);
}
// 字节转化成二进制的函数
function bytesToBinary(bytes) {
return bytes.map(x => lpad(x.toString(2), '0', 8)).join('');
}
// 将二进制转化为十进制字节
function binaryToByte(bin) {
return parseInt(bin, 2);
}
function lpad(str, padString, length) {
while (str.length < length)
str = padString + str;
return str;
}
/* 输出结果
11111010100000101100100010111011001011111101000000101111100000101111111100000001
000101000000011010011111001011100101101000011001
1010
[
'11111010100', '00010110010',
'00101110110', '01011111101',
'00000010111', '11000001011',
'11111100000', '00100010100',
'00000110100', '11111001011',
'10010110100', '00110011010'
]
whisper bind conduct garden actual scrap winter card alley west notable crew
*/
助记词生成种子
现在我们获得了助记词,我们需要将他转化成最终的种子
助记词表示长度为128至256位的熵。 通过使用密钥延伸函数PBKDF2,熵就可以导出成较长的(512位)种子。
PBKDF2的基本原理是通过一个伪随机函数(例如HMAC函数),把明文和一个盐值作为输入参数,然后重复进行运算,并最终产生密钥。如果重复的次数足够大,破解的成本就会变得很高。而盐值的添加也会增加“彩虹表”攻击的难度。
盐值可以表示密码(也可以不写),助记词 + 密码 通过 PBKDF2 函数生成(512位)种子,同时可以让我们使用相同的助记词生成不同的种子。

const bip39 = require('bip39');
let mnemonic = 'whisper bind conduct garden actual scrap winter card alley west notable crew'
let password = '1234asd'
let seed = bip39.mnemonicToSeedSync(mnemonic,password).toString('hex')
console.log(seed)
/* 输出结果
4376cef2cd74bee8d3b61a8eb42dd79efcac4920b456a017c8f524fbd76f689501c4f31f1f893fa7
6fb8fdbec7ef5da014772d80549184e1e52f30fc2f17eb32
*/
总结

补充
当然 nodeJs 中的 bip39 包给了我们更加方便的方法,我们无需生成大随机数(熵),就可以直接生成助记词,然后通过助记词和密码生成种子。
const bip39 = require('bip39');
let mnemonic = bip39.generateMnemonic(128)
let seed = bip39.mnemonicToSeedSync(mnemonic,password).toString('hex')
console.log(seed)