如何解决仅使用公钥在 HD 钱包中生成以太坊地址 (bitcoinj/web3j)
我试图为用 bitcoinj 库实现的 HD Wallet 密钥生成以太坊地址,但我感到困惑:
DeterministicSeed seed = new DeterministicSeed("some seed code here",null,"",1409478661L);
DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).build();
DeterministicKey addrKey = chain.getKeyByPath(HDUtils.parsePath("M/44H/60H/0H/0/0"),true);
System.out.println("address from pub=" + Keys.getAddress(Sign.publicKeyFromPrivate(addrKey.getPrivKey())));
此代码根据 https://iancoleman.io/bip39/ 打印正确的以太坊地址。这里一切都很好。
但是当我试图避免使用私钥并仅使用公钥生成非强化密钥时,我得到了不同的结果,即调用返回另一个结果:
System.out.println("address from pub=" + Keys.getAddress(addrKey.getPublicKeyAsHex()));
看起来问题出在“不同的公钥”上,即 Sign.publicKeyFromPrivate(addrKey.getPrivKey())
和 addrKey.getPublicKeyAsHex()
的结果不同。
我对密码学没有经验,因此这可能是一个愚蠢的问题......但我很感激这里的任何建议。
解决方法
与比特币一样,Ethereum 使用 secp256k1。 Ethereum addresses 推导如下:
- 第 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
as 也可以通过网站 https://iancoleman.io/bip39/ 进行验证。
第 1 步:
在发布的问题中,表达式 Sign.publicKeyFromPrivate()
和 addrKey.getPublicKeyAsHex()
提供了不同的结果。这两个函数都返回不同类型的公钥。 Sign.publicKeyFromPrivate()
使用 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 坐标。这是公钥的 compressed 格式。如果 y 值是偶数(如本例所示),则前导字节的值为 0x02,或者值为 0x03。由于压缩格式不包含 y 坐标,因此不能用于直接推断以太坊地址,否则无论如何都会导致地址错误(间接,当然,它这是可能的,因为 y 坐标可以从压缩的公钥中导出)。
可以获取公钥的uncompressed格式,例如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
并删除标记字节:
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,uncompressed.length - 1)));
System.out.println(Keys.getAddress(Sign.publicKeyFromPrivate(addrKey.getPrivKey()))); // For comparison
按预期返回相同的以太坊地址:
23ad59cc6afff2e508772f69d22b19ffebf579e7
23ad59cc6afff2e508772f69d22b19ffebf579e7
23ad59cc6afff2e508772f69d22b19ffebf579e7
这里要注意的一点是,Keys.getAddress(byte[]
) 不会填充传递的 byte[]
,而 BigInteger
或十六进制字符串的重载隐式填充。这可能是相关的,例如将 BigInteger
(例如由 Sign.publicKeyFromPrivate(addrKey.getPrivKey())
提供)转换为 byte[]
时,因为结果也可能少于 64 个字节(这将导致不同的 Keccak-256 em> 哈希)。如果在这种情况下使用 Keys.getAddress(byte[])
,它必须用前导 0x00 值填充 explicitly
,最多 64 个字节。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。