https://aaron67.cc/2019/01/22/bitcoin-wallet/
在收发比特币时会使用专门的桌面软件或手机 App,这类应用程序统称为比特币“钱包”。
通过之前文章的介绍,你知道:
所以,
钱包软件涵盖的功能大致有:
了解钱包的工作原理十分必要,这能让你更高效的使用软件,更安全的“存储”(其实是保护私钥)和收发比特币。
这篇文章介绍比特币钱包的幕后细节。
我们常说,比特币交易是
出于隐私保护的目的,一个比特币地址只应被使用一次,这样能避免别有用心之人根据已知信息追踪到你其他的交易活动(社会工程学)。
出于安全的考虑,也应该这么做,当你不小心泄露了某个地址的私钥时,不至于损失所有的比特币。
一般的,
比特币的全节点软件一般都包含钱包功能,这种钱包只是随机生成私钥的集合(JBOK,Just a Bunch of Keys),称为不确定性钱包或随机钱包。
一个私钥,会对应唯一的公钥和地址,当你每次都使用新地址收发比特币时,会生成大量的私钥。
这些私钥之间彼此独立,毫无关联,意味着你需要经常备份用过的私钥,否则一旦钱包软件不可访问,你的比特币也会石沉大海。
由于在备份和使用时过于麻烦,不确定性钱包已不再被推荐使用,逐渐被确定性钱包取代。
另外,全节点软件会下载所有的区块数据,如果你从头开始,这将是一个漫长的同步过程,通常需要几天时间,并占用掉几百 G 的硬盘空间。
从功能实现的角度看,钱包只需要“知道”那些与自己私钥有关的交易和区块便可正常工作,并不需要下载所有的区块数据。
现在的钱包软件基本都是开箱即用的,只需同步少量数据便可直接使用,十分方便。
确定性钱包中的私钥都可以从一个随机种子(Seed)计算出来,计算过程是单向的,你无法从私钥计算出种子的内容。
这种钱包在备份和迁移时十分方便,备份一个种子就相当于备份了钱包中的所有私钥,向新钱包中导入种子就可以恢复所有私钥。
确定性钱包从逻辑上看,是下面的样子。
下列 BIP 共同定义了一种确定性钱包的实现,这种钱包被称为分层确定性(HD,Hierarchical Deterministic)钱包。
除此之外,还有
“分层”的意思是,钱包的中的私钥具有层级关系,BIP-32 定义了私钥间的树形结构。
“确定性”的意思是,当种子(Seed)确定后,钱包中的所有私钥便都是确定的,都可以从这个种子计算出来,相同的种子计算出的私钥也都是相同的。
基于树形结构,HD 钱包的一个父密钥可以衍生出一系列子密钥,每个子密钥又可以继续衍生出一系列孙密钥,依次类推无限衍生下去,就像下图所示的样子。
现在主流的钱包软件基本都是兼容 BIP-32、BIP-39 和 BIP-44 的 HD 钱包。
当你用 https://www.bitaddress.org/ 生成一个私钥时,可以通过随意晃动鼠标和敲击键盘来引入更多的随机性。
钱包软件在创建私钥时,都需要引入类似这样的随机性以保证密码学上的私钥安全。
对 HD 钱包来说也是一样,一切计算工作都从一个随机序列开始。
L + L 32 = 33 L 32 = 11 × 3 L 32 L + \frac{L}{32} = \frac{33L}{32} = 11 \times \frac{3L}{32} L+32L=3233L=11×323L
对下面 128 位的随机序列:
5e5d507dc5d543a8c7415656dac4ba0c
计算助记词的过程为
# S1
5e5d507dc5d543a8c7415656dac4ba0c
# S2 = SHA256(S1)
# http://bit.ly/2Hv5Loe
055a1b4dc3af3267362e5d89b707fac6a94ef40a7be5f20f0940de178f01ea33
# 取高 4 位作为校验和
# S3 = S1 + Checksum
5e5d507dc5d543a8c7415656dac4ba0c 0
# S3 的二进制串
01011110010111010101000001111101110001011101010101000011101010001100011101000001010101100101011011011010110001001011101000001100 0000
# 从高位到低位将 S3 每 11 位分成一组
01011110010 # 754
11101010100 # 1876
00011111011 # 251
10001011101 # 1117
01010100001 # 673
11010100011 # 1699
00011101000 # 232
00101010110 # 342
01010110110 # 694
11010110001 # 1713
00101110100 # 372
00011000000 # 192
从英语单词表中查找这 12 个数对应的单词,得到这个随机序列对应的助记词为
furnace tunnel buyer merry feature stamp brown client fine stomach company blossom
注意,
种子由助记词计算而来,使用 PBKDF2(Password-Based Key Derivation Function 2)方法。
mnemonic
和用户指定的密语(Passphrase)拼接而成,这个密语是可选的写一段程序从助记词计算种子。
package main
import (
"encoding/hex"
"fmt"
"github.com/tyler-smith/go-bip39"
)
func main() {
mnemonic := "furnace tunnel buyer merry feature stamp brown client fine stomach company blossom"
fmt.Println(hex.EncodeToString(bip39.NewSeed(mnemonic, "")))
fmt.Println(hex.EncodeToString(bip39.NewSeed(mnemonic, "bitcoin")))
}
// 输出
// 2588c36c5d2685b89e5ab06406cd5e96efcc3dc101c4ebd391fc93367e5525aca6c7a5fe4ea8b973c58279be362dbee9a84771707fc6521c374eb10af1044283
// 1e8340ad778a2bbb1ccac4dd02e6985c888a0db0c40d9817998c0ef3da36e846b270f2c51ad67ac6f51183f567fd97c58a31d363296d5dc6245a0a3c4a3e83c5
使用这个 Node.js 库,可以看到 PBKDF2 的更多细节。
const crypto = require('crypto');
const hash = 'sha512'
const round = 2048
const seed_bytes = 64
var mnemonic = 'furnace tunnel buyer merry feature stamp brown client fine stomach company blossom'
var passphrase = ''
var salt = 'mnemonic' + passphrase
crypto.pbkdf2(mnemonic, salt, round, seed_bytes, hash, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex'));
});
对于上面得到的助记词,在密语为空时,计算出的种子为
2588c36c5d2685b89e5ab06406cd5e96efcc3dc101c4ebd391fc93367e5525aca6c7a5fe4ea8b973c58279be362dbee9a84771707fc6521c374eb10af1044283
如果密语为bitcoin
,计算出的种子为
1e8340ad778a2bbb1ccac4dd02e6985c888a0db0c40d9817998c0ef3da36e846b270f2c51ad67ac6f51183f567fd97c58a31d363296d5dc6245a0a3c4a3e83c5
你能看到,
HD 钱包的确定性来源于种子,当种子确定后,钱包中的所有私钥就都是确定的,都可以从种子计算出来。
所以你可以直接记录下这个种子的值,作为 HD 钱包的备份,只不过这一大串内容抄写起来有点麻烦。
对一个 HD 钱包,初始化种子的过程涉及到两个变量:
所以在备份 HD 钱包时,需要同时备份助记词和密语,这样就相当于备份了整个钱包内的所有私钥。
HD 钱包中的私钥是树状的层级结构。
BIP-32 定义,HD 钱包使用 HMAC-SHA512 方法从种子衍生主私钥。
HMAC-SHA512 使用 SHA512 哈希算法,以一个消息(Message)和一个密钥(Key)作为输入,生成 512 位(64 字节)的消息摘要(Digest)作为输出。
从种子计算主私钥时,种子作为输入的消息,字符串Bitcoin seed
作为输入的密钥,计算产生 512 位的输出。
package main
import (
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"fmt"
"github.com/tyler-smith/go-bip39"
)
func main() {
mnemonic := "furnace tunnel buyer merry feature stamp brown client fine stomach company blossom"
seed := bip39.NewSeed(mnemonic, "")
fmt.Println(hex.EncodeToString(seed))
hmacSHA512 := hmac.New(sha512.New, []byte("Bitcoin seed"))
hmacSHA512.Write(seed)
digest := hmacSHA512.Sum(nil)
fmt.Println("Master private key\t" + hex.EncodeToString(digest[:32]))
fmt.Println("Master chain code\t" + hex.EncodeToString(digest[32:]))
}
// 输出
// 2588c36c5d2685b89e5ab06406cd5e96efcc3dc101c4ebd391fc93367e5525aca6c7a5fe4ea8b973c58279be362dbee9a84771707fc6521c374eb10af1044283
// Master private key 116c2daffad72d24cd3c122a65f937ec2743f98952e174ae158bf6ea70c78954
// Master chain code a74b75701aba81dd29e94226696cc0e67d6a5f29398d151c05c09c416dbf0865
对刚才的种子,计算出的主私钥为
116c2daffad72d24cd3c122a65f937ec2743f98952e174ae158bf6ea70c78954
主链码为
a74b75701aba81dd29e94226696cc0e67d6a5f29398d151c05c09c416dbf0865
这个从种子衍生出的主私钥,跟之前文章介绍的{% post_link bitcoin-keys 比特币私钥 %}没有任何区别,通过 Secp256k1 椭圆曲线乘法,可以计算出其对应的主公钥(Master Public Key):
02c8022cf8de6472f50f08b8b7e364536ab78e25333e0d1e39c0fbf37978ff2f0f
HD 钱包中的每个密钥(私钥和公钥)都有 2 32 2^{32} 232 个子密钥。
每个子密钥都用一个序号标识,表示它是这个父密钥衍生出的第几个子密钥,序号从 0 开始计数。
子密钥衍生(CKD,Child Key Derivation)算法由 BIP-32 定义。
一般的,在衍生子密钥时,将父密钥、序号和父链码作为 CKD 的输入,输出一个 256 位的子密钥和一个 256 位的子链码。
这个子链码会在这个子密钥衍生子密钥时,作为 CKD 的输入。
写个程序算一下,主私钥116c2daffad72d24cd3c122a65f937ec2743f98952e174ae158bf6ea70c78954
的第 0 个和第 1 个子密钥。
package main
import (
"encoding/hex"
"fmt"
"github.com/tyler-smith/go-bip32"
"github.com/tyler-smith/go-bip39"
)
func main() {
mnemonic := "furnace tunnel buyer merry feature stamp brown client fine stomach company blossom"
seed := bip39.NewSeed(mnemonic, "")
masterKey, _ := bip32.NewMasterKey(seed)
fmt.Println("Master private key\t" + hex.EncodeToString(masterKey.Key))
fmt.Println("Master public key\t" + hex.EncodeToString(masterKey.PublicKey().Key))
fmt.Println("Master chain code\t" + hex.EncodeToString(masterKey.ChainCode))
InspectChildKey(masterKey, 0)
InspectChildKey(masterKey, 1)
}
func InspectChildKey(parentKey *bip32.Key, index uint32) {
childKey, _ := parentKey.NewChildKey(index)
fmt.Println(fmt.Sprintf("Child %d private key\t%s", index, hex.EncodeToString(childKey.Key)))
fmt.Println(fmt.Sprintf("Child %d public key\t%s", index, hex.EncodeToString(childKey.PublicKey().Key)))
fmt.Println(fmt.Sprintf("Child %d chain code\t%s", index, hex.EncodeToString(childKey.ChainCode)))
}
// 输出
// Master private key 116c2daffad72d24cd3c122a65f937ec2743f98952e174ae158bf6ea70c78954
// Master public key 02c8022cf8de6472f50f08b8b7e364536ab78e25333e0d1e39c0fbf37978ff2f0f
// Master chain code a74b75701aba81dd29e94226696cc0e67d6a5f29398d151c05c09c416dbf0865
// Child 0 private key 104eedfeeaa8b2c1217c721f375ece3e6981501c7ccd17fcb58867816d01c1b9
// Child 0 public key 0272fed87974babeee6d01b918d87dcd16d5eabc7eab43c66a547ffea47229563a
// Child 0 chain code e2b3998b1df51120f87da3eee1ab6e8b2afb82234d4d6ae62d6e50570e72f737
// Child 1 private key 7d008006853eea7982e25b7ea325a049161ffa3017c5b80095eda8bc1a2ffb98
// Child 1 public key 02473bb5ace2f2e2dce4bf7cb3b89ae329c85612753bcb02db5e5d70d95e86e776
// Child 1 chain code e2061c5a048003ab0c2060761d1452befb2cba0df9f6f4dc17a8db3ec09114b4
根据 BIP-32 的定义,
衍生子密钥时需要将密钥、链码和子密钥序号作为 CKD 的输入,三者缺一不可。
为了方便转录,可以将密钥和链码编码在一起,得到扩展密钥(Extended Key)。
扩展密钥使用 Base58Check 编码,并添加特定的版本前缀。
类型 | 版本前缀的值(十六进制) | Base58Check之后的前缀 |
---|---|---|
扩展私钥 | 0488ade4 | xprv |
扩展公钥 | 0488b21e | xpub |
下面的程序计算例子中的主密钥、主密钥的第 0 个子密钥和主密钥的第 1 个子密钥这三者的扩展密钥。
package main
import (
"fmt"
"github.com/tyler-smith/go-bip32"
"github.com/tyler-smith/go-bip39"
)
func main() {
mnemonic := "furnace tunnel buyer merry feature stamp brown client fine stomach company blossom"
seed := bip39.NewSeed(mnemonic, "")
masterKey, _ := bip32.NewMasterKey(seed)
firstChildKey, _ := masterKey.NewChildKey(0)
secondChildKey, _ := masterKey.NewChildKey(1)
fmt.Println("Master Extended private key\t", masterKey)
fmt.Println("Master Extended public key\t", masterKey.PublicKey())
fmt.Println("Child 0 Extended private key\t", firstChildKey)
fmt.Println("Child 0 Extended public key\t", firstChildKey.PublicKey())
fmt.Println("Child 1 Extended private key\t", secondChildKey)
fmt.Println("Child 1 Extended public key\t", secondChildKey.PublicKey())
}
// 输出
// Master Extended private key xprv9s21ZrQH143K3ixinZag69usQ2CMqDbEkm74p3PWY2ecvUkaiwPGbykMNLfAEwakjwbexs6kKrCDQCGp5vV4yJziz6XB47smbBCmsYhP85Z
// Master Extended public key xpub661MyMwAqRbcGD3Btb7gTHrbx42rEgK67z2fcRo86NBboH5jGUhX9n4qDd5LbW56NE3NVxupasmRwZnzN9cNvkvWiG4ZFjY8HqGi1bPXuUR
// Child 0 Extended private key xprv9vcPiWn5V1vJdpNtY5qBb5a9xZd8PxT9beX8fYS4eVxm7vrw52N49uwUqHz5vEQjt28o5MbRT4VZaasJQrBUEWxjGhmcb4LVktACFoJ8DyS
// Child 0 Extended public key xpub69bk82JyKPUbrJTMe7NBxDWtWbTcoRAzxsSjTvqgCqVjzjC5cZgJhiFxgZmKC676T9tuYAvCTJJq73i114XkMTnJ7o14yfx7tbQ6GVf7D4a
// Child 1 Extended private key xprv9vcPiWn5V1vJgMpAdK5KHc9BZYvwfwEJnmU9PnjxCfpTWoNVsg1PPM1rsnseesNdCCoiH3ZW3BVZLuEqskmNnt4jEqhZ79EerwzPR3LvXQo
// Child 1 Extended public key xpub69bk82JyKPUbtqtdjLcKek5v7amS5PxA9zPkCB9Zm1MSPbheRDKdw9LLj3VjeRyCkm8gtfzhzmD6sotgKkD5Dn3KhK1NyYEHACc2cSqrsTb
扩展密钥使用方便,但要注意:
基于多一层安全的考虑,BIP-32 定义了两种子密钥衍生方案。
为了能方便表示密钥间关系,定义了衍生路径(Derivation Path)的概念。
/
分隔m
表示主密钥i
表示第 i i i 个常规衍生的子密钥,即第 i i i 个子密钥i'
表示第 i i i 个硬化衍生的子密钥,即第 ( 2 31 + i ) (2^{31} + i) (231+i) 个子密钥m/0'/1'/2
表示主密钥的第 0 个强化衍生子密钥的第 1 个强化衍生子密钥的第 2 个常规衍生子密钥(树形结构)。
扩展密钥加上衍生路径,可以确定 HD 钱包里的一个密钥及从这个密钥衍生的之后所有层的子密钥(以这个密钥为根的子树)。
HD 钱包里的密钥是树形结构,可以无限层衍生下去,为了能让不同钱包之间相互兼容,BIP-44 对衍生路径提出了一个规范建议。
m / purpose' / coin_type' / account' / change / address_index
purpose
总是设为 44,代表钱包遵循 BIP-44 规范coin_type
代表币种(对应关系),Bitcoin(BTC)用 0 ,Bitcoin Cash(BCH)用 145,Bitcoin SV(BSV)用 236account
代表逻辑上的钱包“账户”,从 0 开始计数change
代表地址类型,为 0 表示是收款地址,为 1 表示是找零地址address_index
是地址索引,从 0 开始计数,表示是第几个地址我初始化了一个 HD 钱包,使用衍生路径m/44'/236'/0'
作为存放 Bitcoin SV(BSV)的“账户”,那么,
m/44'/236'/0'/0/0
对应的地址,第二个收款地址是公钥m/44'/236'/0'/0/1
对应的地址,以此类推m/44'/236'/0'/1/0
,下一次支付找零到的地址会是m/44'/236'/0'/1/1
,以此类推m/44'/236'/1'
m/44'/145'/0'
注意,BIP-44 不是强制标准,你可以随意使用任何衍生路径,只要在备份 HD 钱包的时候务必记住这个路径就好。
在做一些有关 HD 钱包细节的计算时,https://iancoleman.io/bip39/ 这个工具非常不错,推荐给你。
HD 钱包在备份时十分方便。
另外,从扩展公钥可以常规衍生子公钥及对应地址而不用访问扩展私钥或私钥本身,这是 HD 钱包一个很重要的安全特性。
密钥间的树形结构,与机构的部门设置十分相似,如果一家企业准备使用比特币进行财务收支,可以:
m/0'/0'/x'
的扩展公钥交给各销售部门独自管理和使用
m/0'/0'
的扩展公钥交给市场部,市场部可以查阅所有订单的销售记录,同样无法支付比特币m/0'/0'
的扩展私钥交给财务部,财务部可以用这个更上层的扩展私钥,管理整个公司的加密资产配合 BIP-45 定义的 HD 钱包多签方案,可以方便、安全、灵活的管理公司的加密资产。
还记得{% post_link bitcoin-keys 《比特币的私钥和公钥》 %}文章最后留下的问题吗?
WIF 压缩和不压缩格式表示的私钥,其结果从长度上看并没有明显的区别,为什么要这么做
我们常说,一个公钥会对应一个确定的地址,因为地址是公钥哈希的编码。但比特币公钥可以用不压缩格式和压缩格式两种方法表示,这样可以计算出两个不同的公钥哈希,对应到两个地址。
对于私钥98fd2a819a382f8e142e38242f6caf2a2f6f58e7fe6ca5f23c5b0818b15b4ba6
,对应的公钥为
x = da52d817a5ae3555f36a94528322eb47016f1334b798f5b4fa614a892dabb3ea
y = f314bf38816673c55c1708cf1e36b55c936db97618d6460c4d223be83bec7788
如果用不压缩格式表示公钥,公钥为
04 da52d817a5ae3555f36a94528322eb47016f1334b798f5b4fa614a892dabb3ea f314bf38816673c55c1708cf1e36b55c936db97618d6460c4d223be83bec7788
对应的 P2PKH 地址为1B4nPuT41LBxzumQqhrPDsd4NF7DZSWgyQ
。
如果用压缩格式表示公钥,公钥为
02 da52d817a5ae3555f36a94528322eb47016f1334b798f5b4fa614a892dabb3ea
对应的 P2PKH 地址为1BJhat1AMGYbT9HYJxVekoCaPaqB9ZyTyF
。
对于早期的钱包软件,都是直接使用不压缩格式的公钥,计算对应的地址用于收款。
后来,人们发现公钥可以用压缩的格式存储,这样可以节省约一半的存储和传输空间,钱包开始逐渐使用压缩格式的公钥。
如果一个钱包软件支持两种格式的公钥,当你向钱包里导入私钥时,钱包就懵逼了,由于不知道你原来使用的是什么格式的公钥,所以需要在区块链里搜索这两个地址上锁定的 UTXO 从而计算出正确的“账户余额”,这会带来混乱。
为了向后兼容,定义了 WIF 压缩格式。
K
或L
为前缀)5
为前缀)这样做的目的就是为了给导入这些私钥的钱包一个信号:钱包需要使用什么格式的公钥来计算地址,搜索区块链。
使用“钱包”软件,能方便的收发比特币。
早期的钱包都是离散私钥钱包,包含在全节点软件中。
确定性钱包从一个种子衍生出钱包内的所有私钥,分层确定性钱包主要由 BIP-32、BIP-39 和 BIP-44 共同定义。
HD 钱包的幕后细节涉及到很多内容,希望这篇文章能帮你理解它们。