网上有大量的文章介绍了ECDSA(椭圆曲线加密)算法来生成以太坊公私钥对,进而生成一个唯一的以太坊地址。其中绝大部分都提到了非压缩公钥生成地址时先进行哈希运算,然后取后40位就是地址了。然而绝知此事要躬为。到底怎么哈希的,公钥和私钥的格式是什么,相信很多人和我一样不清楚!
笔者因为在研究一个东西,需要顺带弄明白以太坊公钥生成地址的细节流程。笔者首先百度了一下,参考了知乎上面的一篇文章《以太坊的私钥、公钥、地址、账户》 。然而摸索半天之后,得不到文章中列出的结果。最后笔者将文章中的私钥导入到MetaMask 之后,让人大跌眼镜的是,最后得到的地址竟然不是文章中的示例地址。看来一定是哪出问题了,于是笔者决定弄清楚这个公钥到地址的清晰生成流程,不能再模模糊糊了。
知乎中这篇文章中有个流程图: 这个流程图很正确,只有简单三步,然而结果却不正确,最有可能出错的地方应该就是 从公钥经keccak-256算法到压缩公钥了。具体的Keccak-256函数是什么呢?不同的语言有不同的实现,这里笔者还是以最常用的Node.js 和ethers 框架来进行这个问题的验证。
很幸运,ethers 框架中有一个直接使用公钥计算地址的函数,下面是该函数的定义及说明:
ethers.utils.computeAddress( publicOrPrivateKey ) ? string< Address >source
Returns the address for publicOrPrivateKey. A public key may be compressed or uncompressed,
and a private key will be converted automatically to a public key for the derivation.
这是已经封装好的计算库,我们欲知道详细过程参考它的源码实现就能得到。这里具体的参考过程我不讲了,下面直接贴出验证成功的脚本(假定叫test.sol ):
const eccrypto = require("eccrypto");
const sha3 = require("js-sha3");
const {ethers,utils} = require("ethers")
const private_key = "18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725";
const my_wallet = new ethers.Wallet(private_key)
const HexCharacters = "0123456789abcdef";
const public_key = my_wallet.publicKey
printPublicKey(public_key)
let new_key = "0x" + public_key.substring(4)
let new_bytes = utils.arrayify(new_key)
new_key = sha3.keccak_256(new_bytes)
let result = "0x" + new_key.substring(24)
result = utils.getAddress(result)
console.log("")
console.log(result)
console.log(result === my_wallet.address)
function printPublicKey(public_key) {
console.log(public_key.substring(2,4))
let half = (public_key.length - 4)/2
console.log(public_key.substring(4, 4+half))
console.log(public_key.substring(4+half))
}
function convertBytesToHexString(value) {
let result = "0x";
for (let i = 0; i < value.length; i++) {
let v = value[i];
result += HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f];
}
return result;
}
从上面的代码中可以看出,使用公钥生成地址有这么几步:
- 移除公钥的前缀04,这个知乎那篇文章中提到:
采用椭圆曲线数字签名算法ECDSA-secp256k1将私钥(32字节)映射成公钥(65字节)(前缀04+X公钥+Y公钥):
但是它却并没有提到计算Keccak-256时要移除前缀。
- 将上面移除
04 后的16进制字符串再次转化为字节数组 - 对上一步得到的字节数组进行keccak_256运算,得到一个长度为64的哈希值(压缩公钥,32字节)。
- 取上面结果的最后40位,就得到了全小写的地址。
- 可选项。将全小写地址转化为校验后的地址。
我们直接node test.js 会得到以下输出:
04
50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352
2cd470243453a299fa9e77237716103abc11a1df38855ed6f2ee187e9c582ba6
0x3E9003153d9A39D3f57B126b0c38513D5e289c3E
true
这样格式的输出是为了和原知乎文章相比较。
至于原文章为什么最后得到的地址是错的,这里笔者重现了一下,原因在于它直接对移除04 后的公钥字符串进行了Keccak-256运算,而并未将其转化为字节数组。
|