HD钱包私钥、公钥、地址生成流程

转载请注明:https://www.cnblogs.com/tkblack/p/12625510.html

前面了解了助记词生成流程,现在完善下整个助记词到地址的完整流程。

其实在很长一段时间里,我都认为助记词和种子是同一个概念,但并非如此,一个叫mnemonic(助记符或助记词),一个叫seed(种子)。

1.首先由助记词生成种子,bip39使用PBKDF2来推出,这个过程具有不可逆性,种子不能推出助记词;

  有必要说一下PBKDF2这个函数,简单点说就是对 “助记词+盐” 做很多次hmac_sha512,显然,这个过程是不可逆的。这里的盐是为了增加安全性,一般的钱包不会让用户去定义,而是由开发者固定。所以,不同的钱包导入相同的助记词可能生成不同的种子,进而产生不同的私钥。具体做了所少次hmac_sha512计算呢?网上一度说是2048次,参照下面的代码分析下,两层for循环,第一层63次,第二层2047次(最后一个for就不考虑了)。因此我的理解是,63 * (2047 + 1)次,我估计说2048是因为传入参数iter的值是2048。

func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {  // 参数依次为:助记词、盐、2048、64、sha512.New
    prf := hmac.New(h, password)
    hashLen := prf.Size()
    numBlocks := (keyLen + hashLen - 1) / hashLen

    var buf [4]byte
    dk := make([]byte, 0, numBlocks*hashLen)
    U := make([]byte, hashLen)
    for block := 1; block <= numBlocks; block++ {
        // N.B.: || means concatenation, ^ means XOR
        // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
        // U_1 = PRF(password, salt || uint(i))
        prf.Reset()
        prf.Write(salt)
        buf[0] = byte(block >> 24)
        buf[1] = byte(block >> 16)
        buf[2] = byte(block >> 8)
        buf[3] = byte(block)
        prf.Write(buf[:4])
        dk = prf.Sum(dk)
        T := dk[len(dk)-hashLen:]
        copy(U, T)

        // U_n = PRF(password, U_(n-1))
        for n := 2; n <= iter; n++ {
            prf.Reset()
            prf.Write(U)
            U = U[:0]
            U = prf.Sum(U)
            for x := range U {
                T[x] ^= U[x]
            }
        }
    }
    return dk[:keyLen]
}

 

2.上一步,得到了64字节的返回,我们将前32字节作为私钥,后32字节作为链码;

  当然,我们实际看到的私钥是经过组装和编码的。具体组装:版本(0x80)+ 私钥原文 + 压缩标志(0x01,非压缩私钥不填充)+ 校验码(4个字节);这里,通过对私钥原文作2次sha256,对结果取前4个字节作为校验码。由此可知,非压缩私钥37字节,压缩私钥38字节,因此,进行BASE58编码后,一般为51(非压缩)或52(压缩)个字符。

 

3.事实上,一个私钥仅生成一个公钥,再生成一个地址,但是我们的钱包却可以产生很多地址。这就和bip32相关了,bip32通过使用 公钥或私钥 + 链码 + 索引号 (索引号占32位)来生成子私钥,而子私钥又能生成孙私钥,这样类推,就可以产生很多私钥。一个私钥可以生成2的32次方个子私钥;索引号小于0x80000000为普通型,采用公钥+链码+索引号生成子私钥,大于等于0x80000000为增强型,采用私钥+链码+索引号生成子私钥。

 

4.公钥或私钥+链码+索引号,进行hmac_sha512运算,得到一个64字节的输出,将前32字节作为新的私钥,后32字节作为新的链码;

 

5.私钥生成公钥,采用ecc算法生成,使用secp256k1曲线推导,推导公式:K = k * G   (k表示私钥,K表示公钥,G表示基点);

  这里不介绍ecc的具体原理,简单说下,就是重定义加法和乘法运算。假设以上公式,k=3,K= G + G + G (满足结合律、交换律);而算法中 P+Q 的结果为经过P和Q的直线(如果Q=P,则是过P点的切线)和secp256k1曲线的另一个交点关于X轴的对称点,所以其实K就是曲线上一个点。

  前面讲到私钥分为压缩和非压缩,所以公钥也有压缩和非压缩形式。我们已经知道K是一个点K(x,y),非压缩私钥: 版本前缀(0x04) + Kx + Ky;显然,我们可以由Kx推导出Ky(因为曲线已经确定),所以我们仅使用Kx就够了,压缩私钥:版本前缀(0x02或0x03) + Kx。需要02和03两个前缀的原因,因为y值可能为偶数,也可能为奇数,因此定义两个前缀用以区分,02表示偶,03表示奇。现在基本都使用压缩公钥。这里,我在想如果压缩公钥取Ky,那么就不需要定义2个前缀了。

 

6.公钥生成地址,使用sha256和ripemd160算法;

  首先对公钥进行sha256运算,再进行ripemd160运算,记录下该值,假设为R;

  对R进行2次sha256运算,结果取前4个字节作为校验码,假设为check;

  拼接:版本前缀(0x00) + R + check,这就是完整的地址了,当然,我们知道R其实就是地址,只是后面我们又附加了一些信息,最后也是BASE58编码得到我们常见形式的地址。

 

整个过程就是这个样子,写得有点乱,如果有什么错误或描述不准确的地方,欢迎指正!

posted @ 2020-04-03 11:43  tkblack  阅读(1922)  评论(0编辑  收藏  举报