从随机数到私钥
比特币是去中心化的,生成地址更是和传统银行中心化方式不同。
地址不需要中心化组织确认是否有重复,使用者也不需要担心会与别人地址重复,因为地址空间的数量达到 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");
const seed = crypto.randomBytes(32);
const priKey = crypto .createHash("sha256") .update(seed) .digest();
console.log(priKey.toString("hex"));
|
到了这里还很简单,就只遇到一个安全的随机数生成器,还有一个 sha256 的哈希函数。其中随机数生成器都是使用密码学安全的算法或者从随机源中获取,一些自己实现的很少,这里的 crypto.randomBytes 就是从计算机内部各种事件生成的。
当然为了随机,你还可以投硬币,只要 256 次即可。还有一些网页的场景,通过随机滑动鼠标进行生成随机种子。
如果使用 Bitcoin core 的钱包的话,可使用下面 RPC 命令进行。
以下为具体命令和步骤。
bitcoind getnewaddress
这个命令会返回地址,因为私钥不能默认返回,地址为 13smYdDuR5S1zy1Ypu8C65bJmZENooC5hm
,我们还需要进行一步。
dumpprivkey 13smYdDuR5S1zy1Ypu8C65bJmZENooC5hm
这样就返回了我们需要的私钥,KxmRvNP7j7ZXGsspiWJi4prmaYYCuNUEh1NCPQTSht3uSHwxyfrc
。
从私钥到公钥
假设上一节中我们生成了一个私钥:
1
| 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD
|
比特币使用 secp256k1 椭圆曲线加密算法(ECC)来生成公钥。椭圆曲线加密算法比较难理解,这里就不详述,比较简单的理解是可以私钥可以看做曲线上的一个点,私钥可以根据某个特定点进行计算来获取公钥,而这个过程基本是不可逆的。
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
那么首先是 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
|
checksum := SHA256x2(version + pubKeyHash)[:4]
payload := version + pubKeyHash + checksum address := bs58(payload)
|
就此比特币地址生成完毕,更具体的流程可参见《精通比特币》第四章《密钥和地址》。