从随机数到比特币地址

Feb 13 2019

从随机数到私钥

比特币是去中心化的,生成地址更是和传统银行中心化方式不同。

地址不需要中心化组织确认是否有重复,使用者也不需要担心会与别人地址重复,因为地址空间的数量达到 2256 个,用十进制表示的话,大约是 1077 ,而可见宇宙被估计只含有 1080 个原子。更通俗话的讲地址重复的可能性比被陨石砸到的的几率还要低。

上述我提到 256 这个数字就是比特币的使用随机熵的比特位数,简单说就是一个 256 bits 的随机数,有时候还会被称作为种子。

一个种子可以直接作为比特币的私钥,不过为了更加安全,通常会再使用 sha256 进行哈希一次后作为私钥。

读到这里,你已经看到一些一些名词,如果你了解非对称加密以及哈希算法的话,接下来就不会太难理解。

如果使用 Node.js 进行生成密钥的话,就像是如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
const crypto = require("crypto");

// 32字节即256位的种子
const seed = crypto.randomBytes(32);
// 生成私钥
const priKey = crypto
.createHash("sha256")
.update(seed)
.digest();

// 打印可读的16 进制字符串
console.log(priKey.toString("hex"));

到了这里还很简单,就只遇到一个安全的随机数生成器,还有一个 sha256 的哈希函数。其中随机数生成器都是使用密码学安全的算法或者从随机源中获取,一些自己实现的很少,这里的 crypto.randomBytes 就是从计算机内部各种事件生成的。

当然为了随机,你还可以投硬币,只要 256 次即可。还有一些网页的场景,通过随机滑动鼠标进行生成随机种子。

如果使用 Bitcoin core 的钱包的话,可使用下面 RPC 命令进行。

以下为具体命令和步骤。

bitcoind getnewaddress

这个命令会返回地址,因为私钥不能默认返回,地址为 13smYdDuR5S1zy1Ypu8C65bJmZENooC5hm,我们还需要进行一步。

dumpprivkey 13smYdDuR5S1zy1Ypu8C65bJmZENooC5hm

这样就返回了我们需要的私钥,KxmRvNP7j7ZXGsspiWJi4prmaYYCuNUEh1NCPQTSht3uSHwxyfrc

从私钥到公钥

假设上一节中我们生成了一个私钥:

1
1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD

比特币使用 secp256k1 椭圆曲线加密算法(ECC)来生成公钥。椭圆曲线加密算法比较难理解,这里就不详述,比较简单的理解是可以私钥可以看做曲线上的一个点,私钥可以根据某个特定点进行计算来获取公钥,而这个过程基本是不可逆的。

image

node.js crypto 模块已经封装了接下来需要的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const priKey = Buffer.from(
"1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD",
"hex"
);

// 获取曲线
const secp256k1 = crypto.createECDH("secp256k1");
// 设置私钥
secp256k1.setPrivateKey(priKey);
// 获取非压缩公钥
const pubKey = secp256k1.getPublicKey(undefined, "uncompressed");
console.log(pubKey.toString("hex"));

// 输出结果
// 04 +
// f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a +
// 07cf33da18bd734c600b96a72bbc4749d5141c90ec8ac328ae52ddfe2e505bdb +

对于输出结果对应椭圆曲线上的坐标点,先忽略 04 那么首先是 x 轴坐标,接着就是 y 轴坐标。

在获取公钥的时候,第二个参数传递 uncompressed 表明公钥取非压缩版本,那么 04 就代表非压缩标识符。

那么相应的还有压缩版本,把获取公钥步骤时第二个参数换成 compressed,那么输出结果就成了: 03 + f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a

数据量上少了一半。03 不是代表压缩版本的意思,而是代表非压缩压缩版本的对应数值是奇数,也就是上面非压缩公钥的最后一个字母 b 也就是 11,那么肯定是奇数,所以是 03。那如果是偶数呢?就换成 02

从公钥到地址

之后我们需要计算 hash160(pubkey),也就是 RIPEMD160(SHA256(K))

1
2
3
4
5
6
7
8
const sha256_result = crypto
.createHash("sha256")
.update(pubKey)
.digest();
const ripemed160_result = crypto
.createHash("ripemd160")
.update(sha256_result)
.digest();

到了这一步的结果 ripemed160_result 基本就代表了比特币地址了,但是并不是日常见到的地址,可以称之为地址公钥。

为了把地址公钥转换成可打印形式,我们需要对它进行 base58check 操作。

Base58 不含 Base64 中的 0(数字 0)、O(大写字母 o)、l(小写字母 L)、I(大写字母 i),以及“+”和“/”两个字符。简而言之,Base58 就是由不包括(0,O,l,I)的大小写字母和数字组成,这样使得地址长度变小而且更容易辨认手写体。

base58 其实和 base64 之类的操作相似。举个都知道的例子,十进制转换成二进制的时候是除 2 取余,逆序排列 的方式,那么 base58 也差不多,不过换成了 除 58 取余,逆序排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const table = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const zero = 0n;
const base = 58n;

const bs58 = (buf: Buffer) => {
const hex = buf.toString("hex");
let x = hex.length === 0 ? zero : BigInt("0x" + hex);
let res = "";

while (x > zero) {
res = table[Number(x % base)] + res;
x = x / base;
}

for (let i = 0; i < hex.length; i += 2) {
if (hex[i] === "0" && hex[i + 1] === "0") {
res = "1" + res;
} else {
break;
}
}
return res;
};

这一步得到就是 base58 之后的结果。不过我们需要 base58check。而 base58check 就是在 base58 之前对数据加上校验和,防止数据出错。

1
2
3
4
5
6
// 原始数据加上 version prefix 进行两次 sha256 运算
// 取前 4 字节作为校验和
checksum := SHA256x2(version + pubKeyHash)[:4]
// 然后进行拼接数据后再进行 base58 就是比特币地址了
payload := version + pubKeyHash + checksum
address := bs58(payload)

就此比特币地址生成完毕,更具体的流程可参见《精通比特币》第四章《密钥和地址》。

nodejs, bitcoin