捅破区块链开发的那层窗户纸,钱包开发(一)
很多人都在说区块链改变世界,什么改变生产关系,这些都有点大。对于我们大众来说,真的能给我们实际的生活带来便利,这就是好的技术。区块链如果用好了,确实有可能在不久的将来给我们带来一些好处,但不管怎么带来好处,有一样东西大家起码要会用,这个东西就是钱包,它算是打开区块链世界的一扇窗户。也是大多数人接触区块链所使用的第一个产品,本文接下来会介绍钱包相关的一些概念,以及如何自己开发一个钱包。
钱包是什么
在我们传统印象中,钱包是放钱的。在当今世界来说,在大城市中生活的人们大多数已经不用钱包,钱包只是用来携带卡的。这就和我们区块链的钱包有些像了,区块链钱包是用来管理账户的,而那些账户的钱是存放在区块链网络上的。我们可以把钱包管理的地址理解为银行卡号,而钱是存在银行里的,说白了也就是一个数字。
钱包的分类
在区块链领域,钱包多种多样。如果按照运行环境区分,可以分为:浏览器钱包(metamask),手机钱包(imtoken),硬件钱包,纸钱包,脑钱包等。比较常用的还是浏览器钱包和手机钱包。
如果按照架构上区分,又可以分为普通钱包和HD(Hierarchical Deterministic 分层确定性)钱包。分层确定性钱包主要来源于比特币改进提案,也就是BIP协议,该协议后来演进为BIP32、39、44。
钱包有哪些概念需要了解呢
归纳起来,下述这些词汇是必须了解的,当然这是针对开发人员,如果仅仅是使用,要求没有这么高。
- 私钥
- 公钥
- 地址
- 助记词
- 签名
- 私钥存储
私钥是最核心的部分,有了私钥可以为所欲为。比特币或以太坊的私钥是通过椭圆曲线算法随机得到,通过私钥可以得到公钥,而公钥通过哈希计算可以得到地址,在比特币以及以太坊网络中,账户地址由公钥2次哈希得到。
助记词则来源于BIP协议的改进,主要原因是私钥不便于保存,因此引入住助记词的概念,通过12个英文单词可以推导出私钥。在BIP44协议中,主私钥可以推导出子私钥,子私钥可以推导出孙私钥,然后再进一步确定账户地址。
签名是区块链开发中的重要技术,只有当交易被签名了之后才不会被抵赖,同时签名也必须不能被伪造。而钱包必须提供的能力之一就是签名!
私钥存储同样有它的意义,因为私钥必须能保存下来,而那一串数字太难记忆,在区块链中,我们可以将私钥存储为keystore信息,这样就可以以文件的形式保存下来,同时指定一个keystore文件的打开密码,这样可以双重安全。
测试代码如下
助记词测试,需要借助bip39协议包,助记词的推导可以参考下图:
在图上,我们看到助记词获得需要2步,第一步是获取Entroy,第二步才是获得助记词,而助记词的神秘面纱其实也就是将Entroy前四位作为校验位放在后面,形成一个132bits的数据,将这些数据12等分切割,每份11bits,这些bits的0或1最终组成一个10进制数,而这个数字可以在助记词库中获得对应的单词。(助记词列表可以参考该链接:助记词库)
// go get -u github.com/tyler-smith/go-bip39
func test_mnemonic() {
//Entropy 生成
b, err := bip39.NewEntropy(128)
if err != nil {
log.Panic("failed to NewEntropy:", err, b)
}
fmt.Println(b)
//生成助记词
nm, err := bip39.NewMnemonic(b)
if err != nil {
log.Panic("failed to NewMnemonic:", err)
}
fmt.Println(nm)
}
执行该函数,可以看到获得了12个助记词。
下面我们来验证一下,如果安装了ganache,会比较直观,它也是一个ether的钱包,属于图形化的钱包工具。我们可以将其助记词拷贝到代码中,用其去推导账户地址。测试代码如下:
func test_ganache() {
nm := "august human human affair mechanic night verb metal embark marine orient million"
// 助记词转化为种子 - > 账户地址
// 先推导路径,再获得钱包
path := MustParseDerivationPath("m/44'/60'/0'/0/0")
wallet, err := NewFromMnemonic(nm, "")
if err != nil {
log.Panic("failed to NewFromMnemonic:", err)
}
account, err := wallet.Derive(path, false)
if err != nil {
log.Panic("failed to Derive:", err)
}
fmt.Println(account.Address.Hex())
path = MustParseDerivationPath("m/44'/60'/0'/0/2")
account, err = wallet.Derive(path, false)
if err != nil {
log.Panic("failed to Derive:", err)
}
fmt.Println(account.Address.Hex())
}
上述代码中,关键要对m/44'/60'/0'/0/0加以理解,这是以太坊账户地址的路径,也是前面介绍分层确定性钱包特有的。m代表master主私钥,44代表协议版本,60代表币种,如果是0则表示比特币,60代笔以太币,接下来的0‘代表账户,后面0代表子层编号,最后0代表子层的账户编号,具体格式如下:m/purpse'/coin_type'/account'/change/address_index。
我们在代码中推导主要先用路径推导得到path,然后将path传递给Derive函数就可以得到对应的账户地址。大家可以通过修改address_index来测试推导不同的账户地址,是否可以和ganache初始化的地址一致。
接下来我们再来介绍一下如何存储私钥,也就是保存为keystore文件。
func test_keystore() {
nm := "august human human affair mechanic night verb metal embark marine orient million"
// 助记词转化为种子 - > 账户地址
// 先推导路径,再获得钱包
path := MustParseDerivationPath("m/44'/60'/0'/0/0")
wallet, err := NewFromMnemonic(nm, "")
if err != nil {
log.Panic("failed to NewFromMnemonic:", err)
}
account, err := wallet.Derive(path, false)
if err != nil {
log.Panic("failed to Derive:", err)
}
fmt.Println(account.Address.Hex())
//得到私钥
pkey, err := wallet.derivePrivateKey(path)
if err != nil {
log.Panic("failed to derivePrivateKey:", err)
}
fmt.Println(*pkey)
key := NewKeyFromECDSA(pkey)
hdks := NewHDKeyStore("./data")
err = hdks.StoreKey(hdks.JoinPath(account.Address.Hex()), key, "123")
if err != nil {
log.Panic("failed to StoreKey:", err)
}
}
这需要修改以太坊的部分代码,自行封装一个包,然后调用以太坊官方的私钥转json算法,最终形成一个json字符串存储在keystore文件中.
{
"address": "aa81dc6057a30e4a13f45c41822ab1ddd4e01f87",
"crypto": {
"cipher": "aes-128-ctr",
"ciphertext": "93e7d2bf85024b799281440b5b135af5b4e3fd8127b716427160b39c06ec8d55",
"cipherparams": {
"iv": "50ef4c9002b3eb5cd9579a89c35df478"
},
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"n": 262144,
"p": 1,
"r": 8,
"salt": "92a2aad76666fd04fe76603b13d20ac9165f8091c9d184c6ff3a863e93e9aa88"
},
"mac": "ba72448077803aff28e6f10d0d8d7538845840e0888dad6a2ab0d5a633a650d7"
},
"id": "308cb593-b920-4485-9726-6a7093a4d43e",
"version": 3
}
这个文件中有账户地址,ciphertext就是加密后的密文,如果想解析并且使用该文件,必须知道创建该文件时指定的密码,当密码正确时可以解析得到正确的mac值,否则解析就会失败。
上述源码可以在此处下载:下载地址。