好书/好文 [学习笔记] 比特币的私钥和公钥

aaron67 · 2018年12月23日 · 67 次阅读
本帖已被设为精华帖!

文章首发我的博客,搬运一份到这来


在介绍了哈希函数非对称加密之后,这篇文章记录与比特币密钥有关的细节

私钥

比特币的私钥(k)是一个随机数

可以是1n-1之间的任何数字,其中n是常数1.158 * 10^77,略小于2^256

生成一个私钥,本质上就是选一个数,只要选的方法不可预测或不可重复,它就是密码学安全的

我们可以用不同的表示法,来表示同一个私钥

格式
十六进制 f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62
WIF不压缩格式 5KiANv9EHEU4o9oLzZ6A7z4xJJ3uvfK2RLEubBtTz1fSwAbpJ2U
WIF压缩格式 L5agPjZKceSTkhqZF2dmFptT5LFrbr6ZGPvP7u4A6dvhTrr71WZ9

WIF是Wallet Import Format的缩写,有压缩和不压缩两种格式,都是Raw格式的私钥经过Base58Check编码之后的结果

在比特币系统中,大多数需要向用户展示的数据都使用Base58Check编码,它可以压缩数据,提高可读性,避免歧义,并且包含错误校验,能有效防止数据在转录过程中产生错误

Base58Check编码

Base64编码使用了26个小写字母、26个大写字母、10个数字以及两个符号(“+”和“/”),用于在电子邮件这样的基于文本的媒介中传输二进制数据

Base58Base64编码格式的子集,舍弃了一些容易错读和在特定字体中容易混淆的字符,不使用Base64中的0(数字0)、O(大写字母o)、l(小写字母L)、I(大写字母i),以及+/字符,是比特币中使用的一种独特的编码方式

Base58Check在最后编码时使用Base58,并加入了校验和,是一种可逆的操作。对于要编码的数据payload

  1. payload前添加版本前缀Version,得到S1
  2. S1做两次SHA256哈希运算得到S2,取S2的前4字节作为校验和Checksum
  3. S1后附上Checksum,得到S3
  4. S3Base58编码,得到结果

对于不同类型的数据,添加不同的版本前缀,能产生易于辨识的不同结果

类型 版本前缀的值(十六进制) Base58Check之后的前缀
P2PK/P2PKH地址 00 1
P2SH地址 05 3
测试网络(testnet)地址 6F m 或 n
WIF格式的私钥 80 5,K 或 L
根据BIP-38标准 加密的私钥 0142 6P
根据BIP-32标准定义的 扩展公钥 0488B21E xpub

WIF

为了方便用户导入私钥,定义了WIF,分为不压缩和压缩两种类型

WIFBase58Check编码之后的结果,根据上面的表格,WIF在编码时使用的版本前缀为0x80

对于私钥f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62,计算其WIF不压缩格式

// 私钥本身就是payload
f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62

// 下面是Base58Check定义的过程

// 给payload添加版本前缀0x80,得到S1
80 f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62

// 对S1做两次SHA256,得到S2
701ccdd192515bf36a241b9fca879d7915a458cfb36ebcf2c8db1d796dc63b4a

// 取S2的前4字节,得到Checksum
701ccdd1

// S3 = S1 + Checksum
80 f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 701ccdd1

// WIF_uncompressed = Base58(S3),结果以 5 开头
5KiANv9EHEU4o9oLzZ6A7z4xJJ3uvfK2RLEubBtTz1fSwAbpJ2U

稍加改变,在私钥后添加一个压缩标志位,计算结果就是WIF压缩格式

// 在私钥后添加 压缩标志后缀0x01,得到payload
f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 01

// 下面是Base58Check定义的过程

// 给payload添加版本前缀0x80,得到S1
80 f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 01

// 对S1做两次SHA256,得到S2
14372b9cd0d344b679ac30ab70000c245c3c7888907449bccd0caf830a84c2ed

// 取S2的前4字节,得到Checksum
14372b9c

// S3 = S1 + Checksum
80 f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 01 14372b9c

// WIF_uncompressed = Base58(S3),结果以 K 或 L 开头
L5agPjZKceSTkhqZF2dmFptT5LFrbr6ZGPvP7u4A6dvhTrr71WZ9

WIF压缩和不压缩两种形式都使用同样的版本前缀,唯一的区别就是,是否在私钥后添加压缩标志后缀

你可以使用下面这些工具,体验转换的过程

椭圆曲线密码学(ECC,Elliptic Curve Cryptography)

要理解比特币公钥的计算细节,需要先了解一些ECC的背景知识

ECC是一种非对称加密算法,基于椭圆曲线上的离散对数数学问题

y^2 = x^3 - x + 1定义了一条椭圆曲线,函数图像如下

Imgur

比特币使用的椭圆曲线,为

y^2 mod p = (x^3 + 7) mod p

这条椭圆曲线由Secp256k1标准定义,其中

  • mod是取模运算,即取余数,5 mod 2 = 1
  • p是定值2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1,这是一个非常大的素数

因为方程两端mod p运算的存在,所以这条曲线的函数图像并不是连续的,而是二维空间的一系列散开的点

简单点,如果p = 17,它的函数图像如下

Imgur

你可以把secp256k1的函数图像,想象成一个极大的网格上一系列更为复杂的散点

椭圆曲线上的两点p1p2,定义p = p1 + p2为椭圆曲线上点的加法运算

  • 过点p1p2做直线与椭圆曲线相交点q
  • X轴对称翻转点q,得到点p

Imgur

如果p1p2为同一个点,则过p1p2的连线,变成了过该点的椭圆曲线的切线

使用这个工具,可以有更多可视化的直观体验

乘法的定义可以从加法扩展,k为整数

k * p = p + p + ... + p (k个p相加)

下图展示了从椭圆曲线上的点G,计算2G4G8G的操作

注意,为了有直观的理解,图片中的椭圆曲线都是连续的。比特币使用的Secp256k1曲线,其图像并不连续,但它们的数学原理是相同的

公钥

比特币的公钥(K),是Secp256k1定义的椭圆曲线上的一个点

K = k * G

其中,k为私钥(一个整数),G为椭圆曲线上的一个固定点,这个点由Secp256k1标准定义,它的坐标(十六进制表示)为

G.x = 79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798
G.y = 483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8

比特币的私钥(k)和公钥(K)之间的关系是固定的,数学原理保证计算过程单向不可逆,能轻而易举的从私钥计算出其对应的公钥,反过来则无法实现

刚才的例子中,私钥f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62,其对应的公钥为

x = e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789
y = 97693d32c540ac253de7a3dc73f7e4ba7b38d2dc1ecc8e07920b496fb107d6b2

公钥也有两种表示法,压缩格式和不压缩格式

在公钥坐标前添加前缀0x04,可以直接得到不压缩格式的公钥

04 e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789 97693d32c540ac253de7a3dc73f7e4ba7b38d2dc1ecc8e07920b496fb107d6b2

为了表示不压缩格式的公钥,需要65字节,1字节前缀,32字节X坐标,32字节Y坐标

根据Secp256k1曲线的特点,如果知道公钥X坐标的值和Y坐标的奇偶,就可以直接推算出其Y坐标的值。定义压缩格式的公钥

  • 如果Y坐标的值为偶数,在X坐标前添加前缀0x02
  • 如果Y坐标的值为奇数,在X坐标前添加前缀0x03

得到压缩格式的公钥为

02 e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789

为了表示压缩格式的公钥,需要33字节,1字节前缀,32字节X坐标

你可以参考下面的GoLang代码,自己试一试

package main

import (
    "encoding/hex"
    "fmt"
    "github.com/decred/dcrd/dcrec/secp256k1"
)

func main() {
    privateKeyBytes, _ := hex.DecodeString("f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62")
    _, publicKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)
    fmt.Println("pk.x = " + hex.EncodeToString(publicKey.X.Bytes()))
    fmt.Println("pk.y = " + hex.EncodeToString(publicKey.Y.Bytes()))

    fmt.Println("uncompressed public key = " + hex.EncodeToString(publicKey.SerializeUncompressed()))
    fmt.Println("compressed public key = " + hex.EncodeToString(publicKey.SerializeCompressed()))
}

// 输出
// pk.x = e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789
// pk.y = 97693d32c540ac253de7a3dc73f7e4ba7b38d2dc1ecc8e07920b496fb107d6b2
// uncompressed public key = 04e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd78997693d32c540ac253de7a3dc73f7e4ba7b38d2dc1ecc8e07920b496fb107d6b2
// compressed public key = 02e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789

总结

  • 比特币的私钥,本质是一个数
  • 选取私钥的过程如果不可预测或不可重复(是随机的),则私钥是密码学安全的
  • 私钥可以用WIF压缩WIF不压缩格式表示,WIF压缩格式的私钥前缀是KLWIF不压缩格式的私钥前缀是5
  • WIF格式使用了Base58Check编码,这是一个可逆编码,它包含版本前缀、数据块和校验和三部分,容易识别,方便转录
  • ECC是一种基于椭圆曲线数学问题的非对称加密算法,比特币使用Secp256k1标准定义的一条特殊的椭圆曲线,它的函数图像是一堆复杂的散点
  • 比特币的公钥,本质是Secp256k1曲线上的一个点,由其对应的私钥计算得出
  • 椭圆曲线的数学特性保证,从比特币私钥计算其对应公钥是一个单向运算,无法通过公钥计算出其对应的私钥
  • 公钥可以用压缩和不压缩两种格式表示,压缩格式的公钥前缀是0x020x03,不压缩格式的公钥前缀是0x04
  • 没有“压缩的私钥”和“压缩的公钥”,“压缩”只是针对其表示方法,而不是针对私钥和公钥本身

你可能注意到,公钥用压缩格式表示时,有效降低了存储空间,但WIF压缩和不压缩格式,其结果从长度上看并没有明显的区别

为什么要这么做?接下来的两篇文章中说

参考

共收到 0 条回复
aaron67 将本帖设为了精华贴 12月23日 14:52
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册