与比特币一样,以太坊使用secp256k1。以太坊地址派生如下:
- 第 1 步:公钥的 32 字节 x 和 y 坐标连接到 64 字节(如果需要,x 和 y 坐标都用前导 0x00 值填充)。
- 第 2 步:由此生成Keccak-256哈希。
- 第 3 步:最后 20 个字节用作以太坊地址。
对于此处使用的示例,密钥是通过以下方式生成的:
String mnemonic = "elevator dinosaur switch you armor vote black syrup fork onion nurse illegal trim rocket combine";
DeterministicSeed seed = new DeterministicSeed(mnemonic, null, "", 1409478661L);
DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).build();
DeterministicKey addrKey = chain.getKeyByPath(HDUtils.parsePath("M/44H/60H/0H/0/0"), true);
这对应于以下公钥和以太坊地址:
X: a35bf0fdf5df296cc3600422c3c8af480edb766ff6231521a517eb822dff52cd
Y: 5440f87f5689c2929542e75e739ff30cd1e8cb0ef0beb77380d02cd7904978ca
Address: 23ad59cc6afff2e508772f69d22b19ffebf579e7
也可以通过网站https://iancoleman.io/bip39/进行验证。
步骤1:
在发布的问题中,表达式Sign.publicKeyFromPrivate()
并addrKey.getPublicKeyAsHex()
提供不同的结果。这两个函数都返回不同类型的公钥。虽然Sign.publicKeyFromPrivate()
使用 a BigInteger
,但addrKey.getPublicKeyAsHex()
提供了一个十六进制字符串。对于直接比较,BigInteger
可以使用 . 转换为十六进制字符串toString(16)
。当两个表达式的结果显示为:
System.out.println(Sign.publicKeyFromPrivate(addrKey.getPrivKey()).toString(16));
System.out.println(addrKey.getPublicKeyAsHex());
得到以下结果:
a35bf0fdf5df296cc3600422c3c8af480edb766ff6231521a517eb822dff52cd5440f87f5689c2929542e75e739ff30cd1e8cb0ef0beb77380d02cd7904978ca
02a35bf0fdf5df296cc3600422c3c8af480edb766ff6231521a517eb822dff52cd
的输出Sign.publicKeyFromPrivate()
长度为 64 字节,对应于步骤 1 中定义的串联 x 和 y 坐标。因此,由此生成的地址是有效的以太坊地址,如发布的问题中所述。
addrKey.getPublicKeyAsHex()
另一方面,的输出对应于以 0x02 值为前缀的 x 坐标。这是公钥的压缩格式。如果 y 值为偶数(如本例所示),则前导字节的值为 0x02,或者值为 0x03。由于压缩后的格式不包含y坐标,所以不能直接用这个来推断以太坊地址,否则无论如何都会导致地址错误(间接的,当然也有可能因为y坐标可以从压缩的公钥中导出)。
可以获取公钥的未压缩addrKey.decompress()
格式,例如:
System.out.println(addrKey.decompress().getPublicKeyAsHex());
这给出了这个结果:
04a35bf0fdf5df296cc3600422c3c8af480edb766ff6231521a517eb822dff52cd5440f87f5689c2929542e75e739ff30cd1e8cb0ef0beb77380d02cd7904978ca
未压缩格式包含一个前导标记字节,其值为 0x04,后跟 x 和 y 坐标。所以如果去掉前导标记字节,就得到了第1步的数据,这是推导以太坊地址所需要的:
System.out.println(addrKey.decompress().getPublicKeyAsHex().substring(2));
这导致:
a35bf0fdf5df296cc3600422c3c8af480edb766ff6231521a517eb822dff52cd5440f87f5689c2929542e75e739ff30cd1e8cb0ef0beb77380d02cd7904978ca
步骤 2 和 3:
步骤 2 和 3 由 执行Keys.getAddress()
。这允许使用未压缩的公钥获取以太坊地址,如下所示:
System.out.println(Keys.getAddress(addrKey.decompress().getPublicKeyAsHex().substring(2)));
System.out.println(Keys.getAddress(Sign.publicKeyFromPrivate(addrKey.getPrivKey()))); // For comparison
这给出了以太坊地址:
23ad59cc6afff2e508772f69d22b19ffebf579e7
23ad59cc6afff2e508772f69d22b19ffebf579e7
重载Keys.getAddress()
:
Keys.getAddress()
为数据类型BigInteger
、十六进制字符串和byte[]
. 如果未压缩的密钥以 给出byte[]
,例如带有addrKey.getPubKeyPoint().getEncoded(false)
,则byte[]
可以在删除标记字节后直接使用 。或者,byte[]
可以在BigInteger
删除标记字节的情况下将 转换为 a:
byte[] uncompressed = addrKey.getPubKeyPoint().getEncoded(false);
System.out.println(bytesToHex(Keys.getAddress(Arrays.copyOfRange(uncompressed, 1, uncompressed.length))).toLowerCase()); // bytesToHex() from https://stackoverflow.com/a/9855338
System.out.println(Keys.getAddress(new BigInteger(1, uncompressed, 1, uncompressed.length - 1)));
System.out.println(Keys.getAddress(Sign.publicKeyFromPrivate(addrKey.getPrivKey()))); // For comparison
正如预期的那样返回相同的以太坊地址:
23ad59cc6afff2e508772f69d22b19ffebf579e7
23ad59cc6afff2e508772f69d22b19ffebf579e7
23ad59cc6afff2e508772f69d22b19ffebf579e7
这里要注意的一件事是Keys.getAddress(byte[]
)不填充传递的byte[]
,而重载 forBigInteger
或十六进制字符串隐式填充。这可能是相关的,例如在将 a BigInteger
(例如由 提供Sign.publicKeyFromPrivate(addrKey.getPrivKey())
)转换为 abyte[]
时,因为结果也可能少于 64 个字节(这将导致不同的 Keccak-256散列)。如果Keys.getAddress(byte[])
在这种情况下使用,则必须explicitly
用前导 0x00 值填充,最长为 64 字节。