4

我试图为使用 bitcoinj 库实现的 HD 钱包密钥生成以太坊地址,但我很困惑:

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())and的结果addrKey.getPublicKeyAsHex()不同。我没有密码学经验,因此这可能是一个愚蠢的问题......但我会很感激这里的任何建议。

4

1 回答 1

5

与比特币一样,以太使用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 字节。

于 2021-01-15T10:46:31.030 回答