仅使用公钥在 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 使用 secp256k1Ethereum 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() 提供十六进制字符串。对于直接比较,可以使用 BigIntegertoString(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 个字节。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...